[Rt-commit] rt branch, smime, updated. rt-3.8.7-145-g7a86519

Ruslan Zakirov ruz at bestpractical.com
Mon Feb 1 16:02:21 EST 2010


The branch, smime has been updated
       via  7a86519d0c0039b80dc1c57468987136e037dac3 (commit)
       via  bf967e87de118ab199ba461268c8c6ed481a912e (commit)
       via  19a00c4a6cb746727d4f0b40b36195602dd1f1e4 (commit)
       via  9c33a4fab36f54455fee038df2bf22ed91b00d8f (commit)
       via  d0a2e3301fe15c3bf2c448ef3edbe6fb37899e61 (commit)
       via  303bab00c3e22f232b0091538fe1328ccf64e494 (commit)
       via  aec5bd62834658ac042246df2f0ff3be2fa4addf (commit)
       via  7941c4d6497d58398c19791703b5309e92f8fd13 (commit)
       via  8a4f064951f7e27f29666ee8c7745706f5f0480c (commit)
       via  84013125d42abf8ec06ace1c41b0e95e41b1c3c3 (commit)
       via  728fac9d394d9706ac04fc6a46c08f36ab01af26 (commit)
       via  4eed6a44791e3e329e678aee87e034c18ffd2274 (commit)
       via  9b82eabc126fff18312f18a4b8f79e5d70128eaf (commit)
       via  465d7c5be125cc3d2b6fc925b2dbc55130e6609b (commit)
       via  7fe58768ead9ec1f35c022cec4d591f123301c74 (commit)
       via  07fc350a87d696bbb6815b32b66118febe1362f6 (commit)
       via  41b0a917ff168befec20b9d1e7f876dfae7df2ae (commit)
       via  5825d4f1d7061bac51c13accfc956652c6f51825 (commit)
       via  0c58710080c84bbb1df42164233f9bf212db0b5b (commit)
       via  03c15b6afd69968351c9c0df596652f778e2fe00 (commit)
       via  15bbb1dba6b14100c55fa989a3d120d73f5b0e15 (commit)
       via  40ba3532023cd465ff8e9301a15c7d1abb8e6c82 (commit)
       via  6dd172cc16b5bd6953101674464913c602fed66c (commit)
       via  6a051d5c1824c9c8323e2330c0759d6201c80fa6 (commit)
       via  ac79375cf3bbbc1de6af662e8ebe882a7429a73d (commit)
       via  e9217aa257fdef5cc44ac46bdd3bc6ee271d217f (commit)
       via  7985edbdc88b4b0fd066510773eb65f598b681c2 (commit)
       via  2a36da90785f714f1208beba10c82344f38e9985 (commit)
       via  6893498d49454b0bfbd22bd38e4cd41a9fb73af3 (commit)
       via  074f780dcd4a3d70f92a2ce02bfeee9fc3416a00 (commit)
       via  0eaa43d21ea04b7c5dc1c7c96212c53f0320a88a (commit)
       via  f74e93eba04c148e1d9f9280e7c2a09ca8ef6b8f (commit)
       via  41cb4d8258d31b86149e72e6298d68c96ad7ac52 (commit)
       via  e5406ae4086fd0aad2e7c82fc350ffa0dd999a65 (commit)
       via  487d028870649d2bbd5bdfd2976d8ea36b6bc1df (commit)
       via  4ee62c60b4be4ef3fe991cabed49e45a136455a2 (commit)
       via  7f61fb4a3a540d8540281dce59221179c12b361b (commit)
       via  410287325ccdcfab05956fd364391290488c11b4 (commit)
       via  1b8ed363b25c440e53cdc1b39934bd1f1746a007 (commit)
       via  079be6f9674aad30af329eb6323b4a038b9594c2 (commit)
       via  5318e3ebec17051933f3361d610141c212eb5065 (commit)
       via  16be52767702036b0f7f8809067b3e81b55cb61c (commit)
       via  0ac17487e40290275d67d49aab47c7152cf66c71 (commit)
       via  4159f0ce6abf0257eb463560ebefcc0c65d71444 (commit)
       via  d0f7ec0cfe9467d13df09ab9bd836515626fe695 (commit)
       via  5f22bdae5b121afcf57ce0c5ee8dd6f236d66d63 (commit)
       via  b8235065481774f200c5e5bbb2ae67c9e2cd65c9 (commit)
       via  dcd510c9d239947c112ad80f9a0bc0e039cef8c2 (commit)
       via  6754d6438b0e636631af6026cb1873166dc07ffc (commit)
       via  a6029e8716dd149801f2745bba56459755afc60b (commit)
       via  9cd8dea24e4581070a9c71a02ad2c67e401d0007 (commit)
      from  ce8a0fc98d2a69246fb7cf30e1763afe9d1cdace (commit)

Summary of changes:
 configure.ac                                       |   14 +
 etc/RT_Config.pm.in                                |   73 +++-
 lib/RT/Attachment_Overlay.pm                       |    6 +-
 lib/RT/Config.pm                                   |   81 ++++-
 lib/RT/Crypt.pm                                    |  258 +++++++++++
 lib/RT/Crypt/Base.pm                               |   25 +
 lib/RT/Crypt/GnuPG.pm                              |  472 +++++++++++---------
 lib/RT/Crypt/SMIME.pm                              |  470 +++++++++++++++++++
 lib/RT/Interface/Email.pm                          |   22 +-
 lib/RT/Interface/Email/Auth/{GnuPG.pm => Crypt.pm} |   85 +++--
 lib/RT/Interface/Email/Auth/GnuPG.pm               |  187 +--------
 lib/RT/Interface/Web/Handler.pm                    |   10 +-
 lib/RT/Test.pm                                     |  156 +++++--
 lib/RT/User_Overlay.pm                             |   10 +-
 share/html/Admin/Elements/ShowKeyInfo              |   37 +-
 share/html/Admin/Queues/Modify.html                |    4 +-
 share/html/Admin/Users/GnuPG.html                  |    4 +-
 share/html/Elements/GnuPG/SelectKeyForEncryption   |    7 +-
 share/html/Elements/GnuPG/SignEncryptWidget        |   12 +-
 share/html/Elements/RT__Ticket/ColumnMap           |    4 +-
 share/html/Ticket/Elements/ShowGnuPGStatus         |   24 +-
 t/data/smime/keys/MailEncrypted.txt                |  112 +++++
 t/data/smime/keys/MailForSend.txt                  |    7 +
 t/data/smime/keys/README                           |   60 +++
 t/data/smime/keys/ca.crt                           |   37 ++
 t/data/smime/keys/ca.key                           |   54 +++
 t/data/smime/keys/recipient.crt                    |  124 +++++
 t/data/smime/keys/recipient.csr                    |   28 ++
 t/data/smime/keys/recipient.key                    |   54 +++
 t/data/smime/keys/sender.csr                       |   28 ++
 t/data/smime/keys/sender at example.com.crt           |   38 ++
 t/data/smime/keys/sender at example.com.key           |   54 +++
 t/data/smime/keys/sender at example.com.pem           |   92 ++++
 t/data/smime/mails/README                          |   16 +
 t/data/smime/mails/simple-txt-enc.eml              |   36 ++
 t/data/smime/mails/with-bin-attachment.eml         |   45 ++
 t/data/smime/mails/with-text-attachment.eml        |   44 ++
 t/mail/crypt-gnupg.t                               |   91 +++--
 t/mail/gnupg-incoming.t                            |    8 +-
 t/mail/smime/smime-incoming.t                      |  237 ++++++++++
 t/mail/smime/smime-outgoing.t                      |   74 +++
 t/web/gnupg-select-keys-on-create.t                |    8 +-
 t/web/gnupg-select-keys-on-update.t                |    8 +-
 43 files changed, 2616 insertions(+), 600 deletions(-)
 create mode 100644 lib/RT/Crypt.pm
 create mode 100644 lib/RT/Crypt/Base.pm
 create mode 100644 lib/RT/Crypt/SMIME.pm
 copy lib/RT/Interface/Email/Auth/{GnuPG.pm => Crypt.pm} (78%)
 create mode 100644 t/data/smime/keys/MailEncrypted.txt
 create mode 100644 t/data/smime/keys/MailForSend.txt
 create mode 100644 t/data/smime/keys/README
 create mode 100644 t/data/smime/keys/ca.crt
 create mode 100644 t/data/smime/keys/ca.key
 create mode 100644 t/data/smime/keys/recipient.crt
 create mode 100644 t/data/smime/keys/recipient.csr
 create mode 100644 t/data/smime/keys/recipient.key
 create mode 100644 t/data/smime/keys/sender.csr
 create mode 100644 t/data/smime/keys/sender at example.com.crt
 create mode 100644 t/data/smime/keys/sender at example.com.key
 create mode 100644 t/data/smime/keys/sender at example.com.pem
 create mode 100644 t/data/smime/mails/README
 create mode 100644 t/data/smime/mails/simple-txt-enc.eml
 create mode 100644 t/data/smime/mails/with-bin-attachment.eml
 create mode 100644 t/data/smime/mails/with-text-attachment.eml
 create mode 100644 t/mail/smime/smime-incoming.t
 create mode 100644 t/mail/smime/smime-outgoing.t

- Log -----------------------------------------------------------------
commit 9cd8dea24e4581070a9c71a02ad2c67e401d0007
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Fri Jan 15 08:21:48 2010 +0300

    make pseudo OO API out of functions, it was always the plan

diff --git a/lib/RT/Crypt/GnuPG.pm b/lib/RT/Crypt/GnuPG.pm
index f9f6337..c8f7694 100644
--- a/lib/RT/Crypt/GnuPG.pm
+++ b/lib/RT/Crypt/GnuPG.pm
@@ -580,6 +580,7 @@ sub SignEncryptRFC3156 {
 }
 
 sub SignEncryptInline {
+    my $self = shift;
     my %args = ( @_ );
 
     my $entity = $args{'Entity'};
@@ -588,19 +589,20 @@ sub SignEncryptInline {
     $entity->make_singlepart;
     if ( $entity->is_multipart ) {
         foreach ( $entity->parts ) {
-            %res = SignEncryptInline( @_, Entity => $_ );
+            %res = $self->SignEncryptInline( @_, Entity => $_ );
             return %res if $res{'exit_code'};
         }
         return %res;
     }
 
-    return _SignEncryptTextInline( @_ )
+    return $self->_SignEncryptTextInline( @_ )
         if $entity->effective_type =~ /^text\//i;
 
-    return _SignEncryptAttachmentInline( @_ );
+    return $self->_SignEncryptAttachmentInline( @_ );
 }
 
 sub _SignEncryptTextInline {
+    my $self = shift;
     my %args = (
         Entity => undef,
 
@@ -689,6 +691,7 @@ sub _SignEncryptTextInline {
 }
 
 sub _SignEncryptAttachmentInline {
+    my $self = shift;
     my %args = (
         Entity => undef,
 
@@ -791,6 +794,7 @@ sub _SignEncryptAttachmentInline {
 }
 
 sub SignEncryptContent {
+    my $self = shift;
     my %args = (
         Content => undef,
 
@@ -1026,6 +1030,7 @@ sub FindProtectedParts {
 =cut
 
 sub VerifyDecrypt {
+    my $self = shift;
     my %args = (
         Entity    => undef,
         Detach    => 1,
@@ -1049,7 +1054,7 @@ sub VerifyDecrypt {
             push @res, { VerifyInline( %$item ) };
             $status_on = $item->{'Data'};
         } elsif ( $item->{'Format'} eq 'Attachment' ) {
-            push @res, { VerifyAttachment( %$item ) };
+            %res = $self->VerifyAttachment( %$item );
             if ( $args{'Detach'} ) {
                 $item->{'Top'}->parts( [
                     grep "$_" ne $item->{'Signature'}, $item->{'Top'}->parts
@@ -1087,9 +1092,10 @@ sub VerifyDecrypt {
     return @res;
 }
 
-sub VerifyInline { return DecryptInline( @_ ) }
+sub VerifyInline { return (shift)->DecryptInline( @_ ) }
 
 sub VerifyAttachment {
+    my $self = shift;
     my %args = ( Data => undef, Signature => undef, Top => undef, @_ );
 
     my $gnupg = new GnuPG::Interface;
@@ -1144,6 +1150,7 @@ sub VerifyAttachment {
 }
 
 sub VerifyRFC3156 {
+    my $self = shift;
     my %args = ( Data => undef, Signature => undef, Top => undef, @_ );
 
     my $gnupg = new GnuPG::Interface;
@@ -1191,6 +1198,7 @@ sub VerifyRFC3156 {
 }
 
 sub DecryptRFC3156 {
+    my $self = shift;
     my %args = (
         Data => undef,
         Info => undef,
@@ -1271,6 +1279,7 @@ sub DecryptRFC3156 {
 }
 
 sub DecryptInline {
+    my $self = shift;
     my %args = (
         Data => undef,
         Passphrase => undef,
@@ -1320,7 +1329,7 @@ sub DecryptInline {
             seek $block_fh, 0, 0;
 
             my ($res_fh, $res_fn);
-            ($res_fh, $res_fn, %res) = _DecryptInlineBlock(
+            ($res_fh, $res_fn, %res) = $self->_DecryptInlineBlock(
                 %args,
                 GnuPG => $gnupg,
                 BlockHandle => $block_fh,
@@ -1358,6 +1367,7 @@ sub DecryptInline {
 }
 
 sub _DecryptInlineBlock {
+    my $self = shift;
     my %args = (
         GnuPG => undef,
         BlockHandle => undef,
@@ -1408,6 +1418,7 @@ sub _DecryptInlineBlock {
 }
 
 sub DecryptAttachment {
+    my $self = shift;
     my %args = (
         Top  => undef,
         Data => undef,
@@ -1441,7 +1452,7 @@ sub DecryptAttachment {
     $args{'Data'}->bodyhandle->print( $tmp_fh );
     seek $tmp_fh, 0, 0;
 
-    my ($res_fh, $res_fn, %res) = _DecryptInlineBlock(
+    my ($res_fh, $res_fn, %res) = $self->_DecryptInlineBlock(
         %args,
         GnuPG => $gnupg,
         BlockHandle => $tmp_fh,
@@ -1460,6 +1471,7 @@ sub DecryptAttachment {
 }
 
 sub DecryptContent {
+    my $self = shift;
     my %args = (
         Content => undef,
         Passphrase => undef,
@@ -2363,7 +2375,7 @@ sub DrySign {
         Data    => ['t'],
     );
 
-    my %res = SignEncrypt(
+    my %res = $self->SignEncrypt(
         Sign    => 1,
         Encrypt => 0,
         Entity  => $mime,

commit a6029e8716dd149801f2745bba56459755afc60b
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Fri Jan 15 08:23:19 2010 +0300

    first pass on generic Crypt API for RT

diff --git a/lib/RT/Crypt.pm b/lib/RT/Crypt.pm
new file mode 100644
index 0000000..b086333
--- /dev/null
+++ b/lib/RT/Crypt.pm
@@ -0,0 +1,114 @@
+
+use strict;
+use warnings;
+
+package RT::Crypt;
+
+require RT::Crypt::GnuPG;
+require RT::Crypt::SMIME;
+
+sub EnabledOnIncoming {
+    return 'GnuPG', 'SMIME';
+}
+
+# encryption and signatures can be nested one over another, for example:
+# GPG inline signed text can be signed with SMIME
+
+sub FindProtectedParts {
+    my $self = shift;
+    my %args = (
+        Entity => undef,
+        Skip => {},
+        Scattered => 1,
+        @_
+    );
+
+    my $entity = $args{'Entity'};
+    return () if $args{'Skip'}{ $entity };
+
+    my @protocols = $self->EnabledOnIncoming;
+
+    foreach my $protocol ( @protocols ) {
+        my $class = 'RT::Crypt::'. $protocol;
+        my %info = $class->CheckIfProtected( Entity => $entity );
+        next unless keys %info;
+
+        $args{'Skip'}{ $entity } = 1;
+        $info{'Protocol'} = $protocol;
+        return \%info;
+    }
+
+    # not protected itself, look inside
+    push @res, $self->FindProtectedParts(
+        %args, Entity => $_, Scattered => 0,
+    ) foreach grep !$args{'Skip'}{$_}, $entity->parts;
+
+    if ( $args{'Scattered'} ) {
+        my $filter; $filter = sub {
+            return grep !$args{'Skip'}{$_},
+                $_[0]->is_multipart ? () : $_[0],
+                map $filter->($_), grep !$args{'Skip'}{$_},
+                    $_[0]->parts;
+        };
+        my @parts = $filter->($entity);
+        foreach my $protocol ( @protocols ) {
+            my $class = 'RT::Crypt::'. $protocol;
+            my @list = $class->FindScatteredParts( Parts => \@parts, Skip => $args{'Skip'} );
+            next unless @list;
+
+            push @res, @list;
+            @parts = grep !$args{'Skip'}{$_}, @parts;
+        }
+    }
+
+    return @res;
+}
+
+sub SignEncrypt {
+    my $self = shift;
+    my %args = (@_);
+
+    my $entity = $args{'Entity'};
+    if ( $args{'Sign'} && !defined $args{'Signer'} ) {
+        $args{'Signer'} =
+            $self->UseKeyForSigning
+            || (Email::Address->parse( $entity->head->get( 'From' ) ))[0]->address;
+    }
+    if ( $args{'Encrypt'} && !$args{'Recipients'} ) {
+        my %seen;
+        $args{'Recipients'} = [
+            grep $_ && !$seen{ $_ }++, map $_->address,
+            map Email::Address->parse( $entity->head->get( $_ ) ),
+            qw(To Cc Bcc)
+        ];
+    }
+
+    my $using = delete $args{'Using'} || 'GnuPG';
+    my $class = 'RT::Crypt::'. $using;
+
+    return $class->SignEncrypt( %args );
+}
+
+sub VerifyDecrypt {
+    my $self = shift;
+    my %args = (
+        Entity    => undef,
+        Detach    => 1,
+        SetStatus => 1,
+        AddStatus => 0,
+        @_
+    );
+
+    my @res;
+
+    my @protected = $self->FindProtectedParts( Entity => $args{'Entity'} );
+    foreach my $protected ( @protected ) {
+        my $protocol = $protected->{'Protocol'};
+        my $class = 'RT::Crypt::'. $protocol;
+        my %res = $class->VerifyDecrypt( %args, %$protected );
+        push @res, \%res;
+    }
+    return @res;
+}
+
+1;

commit 6754d6438b0e636631af6026cb1873166dc07ffc
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Fri Jan 15 08:24:30 2010 +0300

    switch SignEncrypt to new API
    
    RT::Crypt now drives this and prepares required data

diff --git a/lib/RT/Crypt/GnuPG.pm b/lib/RT/Crypt/GnuPG.pm
index c8f7694..4e93ca6 100644
--- a/lib/RT/Crypt/GnuPG.pm
+++ b/lib/RT/Crypt/GnuPG.pm
@@ -395,27 +395,13 @@ Returns a hash with the following keys:
 =cut
 
 sub SignEncrypt {
-    my %args = (@_);
+    my $self = shift;
 
-    my $entity = $args{'Entity'};
-    if ( $args{'Sign'} && !defined $args{'Signer'} ) {
-        $args{'Signer'} = UseKeyForSigning()
-            || (Email::Address->parse( $entity->head->get( 'From' ) ))[0]->address;
-    }
-    if ( $args{'Encrypt'} && !$args{'Recipients'} ) {
-        my %seen;
-        $args{'Recipients'} = [
-            grep $_ && !$seen{ $_ }++, map $_->address,
-            map Email::Address->parse( $entity->head->get( $_ ) ),
-            qw(To Cc Bcc)
-        ];
-    }
-    
     my $format = lc RT->Config->Get('GnuPG')->{'OutgoingMessagesFormat'} || 'RFC';
     if ( $format eq 'inline' ) {
-        return SignEncryptInline( %args );
+        return $self->SignEncryptInline( @_ );
     } else {
-        return SignEncryptRFC3156( %args );
+        return $self->SignEncryptRFC3156( @_ );
     }
 }
 

commit dcd510c9d239947c112ad80f9a0bc0e039cef8c2
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Fri Jan 15 08:28:46 2010 +0300

    re-write FindProtectedParts
    
    we have to split this functionality into two separate steps:
    1) CheckIfProtected
    2) FindScatteredParts

diff --git a/lib/RT/Crypt/GnuPG.pm b/lib/RT/Crypt/GnuPG.pm
index 4e93ca6..22c4ef0 100644
--- a/lib/RT/Crypt/GnuPG.pm
+++ b/lib/RT/Crypt/GnuPG.pm
@@ -875,142 +875,172 @@ sub SignEncryptContent {
     return %res;
 }
 
-sub FindProtectedParts {
-    my %args = ( Entity => undef, CheckBody => 1, @_ );
+sub CheckIfProtected {
+    my $self = shift;
+    my %args = ( Entity => undef, @_ );
+
     my $entity = $args{'Entity'};
 
-    # inline PGP block, only in singlepart
-    unless ( $entity->is_multipart ) {
-        my $io = $entity->open('r');
-        unless ( $io ) {
-            $RT::Logger->warning( "Entity of type ". $entity->effective_type ." has no body" );
-            return ();
-        }
-        while ( defined($_ = $io->getline) ) {
-            next unless /^-----BEGIN PGP (SIGNED )?MESSAGE-----/;
-            my $type = $1? 'signed': 'encrypted';
-            $RT::Logger->debug("Found $type inline part");
-            return {
-                Type    => $type,
-                Format  => 'Inline',
-                Data  => $entity,
-            };
-        }
-        $io->close;
+    # we check inline PGP block later in another sub
+    return () unless $entity->is_multipart;
+
+    # RFC3156, multipart/{signed,encrypted}
+    my $type = $entity->effective_type;
+    return () unless $type = =~ /^multipart\/(?:encrypted|signed)$/;
+
+    unless ( $entity->parts == 2 ) {
+        $RT::Logger->error( "Encrypted or signed entity must has two subparts. Skipped" );
         return ();
     }
 
-    # RFC3156, multipart/{signed,encrypted}
-    if ( ( my $type = $entity->effective_type ) =~ /^multipart\/(?:encrypted|signed)$/ ) {
-        unless ( $entity->parts == 2 ) {
-            $RT::Logger->error( "Encrypted or signed entity must has two subparts. Skipped" );
+    my $protocol = $entity->head->mime_attr( 'Content-Type.protocol' );
+    unless ( $protocol ) {
+        $RT::Logger->error( "Entity is '$type', but has no protocol defined. Skipped" );
+        return ();
+    }
+
+    if ( $type eq 'multipart/encrypted' ) {
+        unless ( $protocol eq 'application/pgp-encrypted' ) {
+            $RT::Logger->info( "Skipping protocol '$protocol', only 'application/pgp-encrypted' is supported" );
             return ();
         }
-
-        my $protocol = $entity->head->mime_attr( 'Content-Type.protocol' );
-        unless ( $protocol ) {
-            $RT::Logger->error( "Entity is '$type', but has no protocol defined. Skipped" );
+        $RT::Logger->debug("Found part encrypted according to RFC3156");
+        return (
+            Type   => 'encrypted',
+            Format => 'RFC3156',
+            Top    => $entity,
+            Data   => $entity->parts(1),
+            Info   => $entity->parts(0),
+        );
+    } else {
+        unless ( $protocol eq 'application/pgp-signature' ) {
+            $RT::Logger->info( "Skipping protocol '$protocol', only 'application/pgp-signature' is supported" );
             return ();
         }
+        $RT::Logger->debug("Found part signed according to RFC3156");
+        return (
+            Type      => 'signed',
+            Format    => 'RFC3156',
+            Top       => $entity,
+            Data      => $entity->parts(0),
+            Signature => $entity->parts(1),
+        );
+    }
+    return ();
+}
+
+sub FindScatteredParts {
+    my $self = shift;
+    my %args = ( Parts => [], Skip => {}, @_ );
 
-        if ( $type eq 'multipart/encrypted' ) {
-            unless ( $protocol eq 'application/pgp-encrypted' ) {
-                $RT::Logger->info( "Skipping protocol '$protocol', only 'application/pgp-encrypted' is supported" );
-                return ();
+    my @res;
+
+    my @parts = @{ $args{'Parts'} };
+
+    # attachments signed with signature in another part
+    {
+        my @file_indices;
+        for (my $i = 0; $i < @parts; $i++ ) {
+            my $part = $parts[ $i ];
+
+            # we can not associate a signature within an attachment
+            # without file names
+            my $fname = $part->head->recommended_filename;
+            next unless $fname;
+
+            my $type = $part->effective_type;
+
+            if ( $type eq 'application/pgp-signature' ) {
+                push @file_indices, $i;
             }
-            $RT::Logger->debug("Found encrypted according to RFC3156 part");
-            return {
-                Type    => 'encrypted',
-                Format  => 'RFC3156',
-                Top   => $entity,
-                Data  => $entity->parts(1),
-                Info    => $entity->parts(0),
-            };
-        } else {
-            unless ( $protocol eq 'application/pgp-signature' ) {
-                $RT::Logger->info( "Skipping protocol '$protocol', only 'application/pgp-signature' is supported" );
-                return ();
+            elsif ( $type eq 'application/octet-stream' && $fname =~ /\.sig$/i ) {
+                push @file_indices, $i;
             }
-            $RT::Logger->debug("Found signed according to RFC3156 part");
-            return {
-                Type      => 'signed',
-                Format    => 'RFC3156',
-                Top     => $entity,
-                Data    => $entity->parts(0),
-                Signature => $entity->parts(1),
-            };
         }
-    }
 
-    # attachments signed with signature in another part
-    my @file_indices;
-    foreach my $i ( 0 .. $entity->parts - 1 ) {
-        my $part = $entity->parts($i);
+        foreach my $i ( @file_indices ) {
+            my $sig_part = $parts[ $i ];
+            my $sig_name = $sig_part->head->recommended_filename;
+            my ($file_name) = $sig_name =~ /^(.*?)(?:\.sig)?$/;
+
+            my ($data_part_idx) =
+                grep $file_name eq ($parts[$_]->head->recommended_filename||''),
+                grep $sig_part  ne  $parts[$_],
+                    0 .. @parts - 1;
+            unless ( defined $data_part_idx ) {
+                $RT::Logger->error("Found $sig_name attachment, but didn't find $file_name");
+                next;
+            }
 
-        # we can not associate a signature within an attachment
-        # without file names
-        my $fname = $part->head->recommended_filename;
-        next unless $fname;
+            my $data_part_in = $parts[ $data_part_idx ];
 
-        if ( $part->effective_type eq 'application/pgp-signature' ) {
-            push @file_indices, $i;
-        }
-        elsif ( $fname =~ /\.sig$/i && $part->effective_type eq 'application/octet-stream' ) {
-            push @file_indices, $i;
+            $RT::Logger->debug("Found signature (in '$sig_name') of attachment '$file_name'");
+
+            $args{'Skip'}{$data_part_in} = 1;
+            $args{'Skip'}{$sig_part} = 1;
+            push @res, {
+                Type      => 'signed',
+                Format    => 'Attachment',
+                Top       => $entity,
+                Data      => $data_part_in,
+                Signature => $sig_part,
+            };
         }
     }
 
-    my (@res, %skip);
-    foreach my $i ( @file_indices ) {
-        my $sig_part = $entity->parts($i);
-        $skip{"$sig_part"}++;
-        my $sig_name = $sig_part->head->recommended_filename;
-        my ($file_name) = $sig_name =~ /^(.*?)(?:\.sig)?$/;
+    # attachments with inline encryption
+    foreach my $part ( @parts ) {
+        next if $args{'Skip'}{$part};
 
-        my ($data_part_idx) =
-            grep $file_name eq ($entity->parts($_)->head->recommended_filename||''),
-            grep $sig_part  ne  $entity->parts($_),
-                0 .. $entity->parts - 1;
-        unless ( defined $data_part_idx ) {
-            $RT::Logger->error("Found $sig_name attachment, but didn't find $file_name");
-            next;
-        }
-        my $data_part_in = $entity->parts($data_part_idx);
+        my $fname = $part->head->recommended_filename || '';
+        next unless $fname =~ /\.pgp$/;
 
-        $skip{"$data_part_in"}++;
-        $RT::Logger->debug("Found signature (in '$sig_name') of attachment '$file_name'");
-        push @res, {
-            Type      => 'signed',
-            Format    => 'Attachment',
-            Top       => $entity,
-            Data      => $data_part_in,
-            Signature => $sig_part,
-        };
-    }
+        $RT::Logger->debug("Found encrypted attachment '$fname'");
 
-    # attachments with inline encryption
-    my @encrypted_indices =
-        grep {($entity->parts($_)->head->recommended_filename || '') =~ /\.pgp$/}
-            0 .. $entity->parts - 1;
-
-    foreach my $i ( @encrypted_indices ) {
-        my $part = $entity->parts($i);
-        $skip{"$part"}++;
-        $RT::Logger->debug("Found encrypted attachment '". $part->head->recommended_filename ."'");
+        $args{'Skip'}{$part} = 1;
         push @res, {
-            Type      => 'encrypted',
-            Format    => 'Attachment',
+            Type    => 'encrypted',
+            Format  => 'Attachment',
             Top     => $entity,
             Data    => $part,
         };
     }
 
-    push @res, FindProtectedParts( Entity => $_ )
-        foreach grep !$skip{"$_"}, $entity->parts;
+    # inline PGP block
+    foreach my $part ( @parts ) {
+        next if $args{'Skip'}{$part};
+
+        my $type = $self->_CheckIfProtectedInline( $entity );
+        next unless $type;
+
+        $args{'Skip'}{$part} = 1;
+        push @res, {
+            Type      => $type,
+            Format    => 'Inline',
+            Data      => $entity,
+        };
+    }
 
     return @res;
 }
 
+sub _CheckIfProtectedInline {
+    my $self = shift;
+    my $entity = shift;
+
+    my $io = $entity->open('r');
+    unless ( $io ) {
+        $RT::Logger->warning( "Entity of type ". $entity->effective_type ." has no body" );
+        return '';
+    }
+    while ( defined($_ = $io->getline) ) {
+        next unless /^-----BEGIN PGP (SIGNED )?MESSAGE-----/;
+        return $1? 'signed': 'encrypted';
+    }
+    $io->close;
+    return '';
+}
+
 =head2 VerifyDecrypt Entity => undef, [ Detach => 1, Passphrase => undef, SetStatus => 1 ]
 
 =cut

commit b8235065481774f200c5e5bbb2ae67c9e2cd65c9
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Fri Jan 15 08:32:29 2010 +0300

    VerifyDecrypt works with one part only now

diff --git a/lib/RT/Crypt/GnuPG.pm b/lib/RT/Crypt/GnuPG.pm
index 22c4ef0..a6fc17f 100644
--- a/lib/RT/Crypt/GnuPG.pm
+++ b/lib/RT/Crypt/GnuPG.pm
@@ -1048,26 +1048,27 @@ sub _CheckIfProtectedInline {
 sub VerifyDecrypt {
     my $self = shift;
     my %args = (
-        Entity    => undef,
+        Info      => undef,
         Detach    => 1,
         SetStatus => 1,
         AddStatus => 0,
         @_
     );
-    my @protected = FindProtectedParts( Entity => $args{'Entity'} );
-    my @res;
-    # XXX: detaching may brake nested signatures
-    foreach my $item( grep $_->{'Type'} eq 'signed', @protected ) {
+
+    my %res;
+
+    my $item = $args{'Info'};
+    if ( $item->{'Type'} eq 'signed' ) {
         my $status_on;
         if ( $item->{'Format'} eq 'RFC3156' ) {
-            push @res, { VerifyRFC3156( %$item, SetStatus => $args{'SetStatus'} ) };
+            %res = $self->VerifyRFC3156( %$item, SetStatus => $args{'SetStatus'} );
             if ( $args{'Detach'} ) {
                 $item->{'Top'}->parts( [ $item->{'Data'} ] );
                 $item->{'Top'}->make_singlepart;
             }
             $status_on = $item->{'Top'};
         } elsif ( $item->{'Format'} eq 'Inline' ) {
-            push @res, { VerifyInline( %$item ) };
+            %res = $self->VerifyInline( %$item );
             $status_on = $item->{'Data'};
         } elsif ( $item->{'Format'} eq 'Attachment' ) {
             %res = $self->VerifyAttachment( %$item );
@@ -1078,6 +1079,8 @@ sub VerifyDecrypt {
                 $item->{'Top'}->make_singlepart;
             }
             $status_on = $item->{'Data'};
+        } else {
+            die "Unknow format '". $item->{'Format'} ."' of GnuPG signed part";
         }
         if ( $args{'SetStatus'} || $args{'AddStatus'} ) {
             my $method = $args{'AddStatus'} ? 'add' : 'set';
@@ -1085,18 +1088,19 @@ sub VerifyDecrypt {
                 'X-RT-GnuPG-Status' => $res[-1]->{'status'}
             );
         }
-    }
-    foreach my $item( grep $_->{'Type'} eq 'encrypted', @protected ) {
+    } elsif ( $item->{'Type'} eq 'encrypted' ) {
         my $status_on;
         if ( $item->{'Format'} eq 'RFC3156' ) {
-            push @res, { DecryptRFC3156( %$item ) };
+            %res = $self->DecryptRFC3156( %$item );
             $status_on = $item->{'Top'};
         } elsif ( $item->{'Format'} eq 'Inline' ) {
-            push @res, { DecryptInline( %$item ) };
+            %res = $self->DecryptInline( %$item );
             $status_on = $item->{'Data'};
         } elsif ( $item->{'Format'} eq 'Attachment' ) {
-            push @res, { DecryptAttachment( %$item ) };
+            %res = $self->DecryptAttachment( %$item );
             $status_on = $item->{'Data'};
+        } else {
+            die "Unknow format '". $item->{'Format'} ."' of GnuPG encrypted part";
         }
         if ( $args{'SetStatus'} || $args{'AddStatus'} ) {
             my $method = $args{'AddStatus'} ? 'add' : 'set';
@@ -1104,6 +1108,8 @@ sub VerifyDecrypt {
                 'X-RT-GnuPG-Status' => $res[-1]->{'status'}
             );
         }
+    } else {
+        die "Unknow type '". $item->{'Type'} ."' of protected item";
     }
     return @res;
 }

commit 5f22bdae5b121afcf57ce0c5ee8dd6f236d66d63
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Mon Jan 18 17:06:21 2010 +0300

    make sure Auth::Crypt is used as mail plugin

diff --git a/lib/RT/Config.pm b/lib/RT/Config.pm
index 6fc1b89..0c03b57 100644
--- a/lib/RT/Config.pm
+++ b/lib/RT/Config.pm
@@ -397,7 +397,29 @@ our %META = (
             $self->Set( DisableGD => 1 );
         },
     },
-    MailPlugins  => { Type => 'ARRAY' },
+    MailPlugins  => {
+        Type => 'ARRAY',
+        PostLoadCheck => sub {
+            my $self = shift;
+            my @plugins = $self->Get('MailPlugins');
+            if ( grep $_ eq 'Auth::GnuPG' || $_ eq 'Auth::SMIME', @plugins ) {
+                $RT::Logger->warning(
+                    'Auth::GnuPG and Auth::SMIME (from an extension) has been'
+                    .' replaced with Auth::Crypt as generic mail plugin for'
+                    .' processing encrypted/signed emails using GnuPG, SMIME'
+                    .' and other security providing solutions.'
+                );
+                my %seen;
+                @plugins =
+                    grep !$seen{$_}++,
+                    grep {
+                        $_ eq 'Auth::GnuPG' || $_ eq 'Auth::SMIME'
+                        ? 'Auth::Crypt' : $_
+                    } @plugins;
+                $self->Set( MailPlugins => @plugins );
+            }
+        },
+    },
     Plugins      => { Type => 'ARRAY' },
     GnuPG        => { Type => 'HASH' },
     GnuPGOptions => { Type => 'HASH',

commit d0f7ec0cfe9467d13df09ab9bd836515626fe695
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Mon Jan 18 17:34:27 2010 +0300

    Auth::Crypt mail plugin out of Auth::GnuPG

diff --git a/lib/RT/Interface/Email/Auth/Crypt.pm b/lib/RT/Interface/Email/Auth/Crypt.pm
new file mode 100644
index 0000000..e24a32c
--- /dev/null
+++ b/lib/RT/Interface/Email/Auth/Crypt.pm
@@ -0,0 +1,292 @@
+# BEGIN BPS TAGGED BLOCK {{{
+# 
+# COPYRIGHT:
+# 
+# This software is Copyright (c) 1996-2009 Best Practical Solutions, LLC
+#                                          <jesse at bestpractical.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/licenses/old-licenses/gpl-2.0.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 }}}
+
+package RT::Interface::Email::Auth::Crypt;
+
+use strict;
+use warnings;
+
+=head1 NAME
+
+RT::Interface::Email::Auth::Crypt - decrypting and verifying protected emails
+
+=head2 DESCRIPTION
+
+This mail plugin decrypts and verifies incomming emails. Supported
+encryption protocols are GnuPG and SMIME.
+
+This code is independant from code that encrypts/sign outgoing emails, so
+it's possible to decrypt data without bringing in encryption. To enable
+it put the module in the mail plugins list:
+
+    Set(@MailPlugins, 'Auth::MailFrom', 'Auth::Crypt', ...other filters...);
+
+=head3 GnuPG
+
+To use the gnupg-secured mail gateway, you need to do the following:
+
+Set up a GnuPG key directory with a pubring containing only the keys
+you care about and specify the following in your SiteConfig.pm
+
+    Set(%GnuPGOptions, homedir => '/opt/rt3/var/data/GnuPG');
+    
+Read also: L<RT::Crypt> and L<RT::Crypt::GnuPG>.
+
+=head3 SMIME
+
+Read also: L<RT::Crypt> and L<RT::Crypt::SMIME>.
+
+=cut
+
+sub ApplyBeforeDecode { return 1 }
+
+use RT::Crypt;
+use RT::EmailParser ();
+
+sub GetCurrentUser {
+    my %args = (
+        Message       => undef,
+        RawMessageRef => undef,
+        @_
+    );
+
+    # we clean all possible headers
+    my @headers =
+        qw(
+            X-RT-Incoming-Encrypton
+            X-RT-Incoming-Signature
+            X-RT-Privacy
+        ),
+        map "X-RT-$_-Status", RT::Crypt->Protocols;
+    foreach my $p ( $args{'Message'}->parts_DFS ) {
+        $p->head->delete($_) for @headers;
+    }
+
+    my @check_protocols = RT::Crypt->EnabledOnIncoming;
+
+    my $msg = $args{'Message'}->dup;
+
+    my ($status, @res) = VerifyDecrypt(
+        Entity => $args{'Message'}, AddStatus => 1,
+    );
+    if ( $status && !@res ) {
+        $args{'Message'}->head->add(
+            'X-RT-Incoming-Encryption' => 'Not encrypted'
+        );
+        return 1;
+    }
+
+    unless ( $status ) {
+        $RT::Logger->error("Had a problem during decrypting and verifying");
+        my $reject = HandleErrors( Message => $args{'Message'}, Result => \@res );
+        return (0, 'rejected because of problems during decrypting and verifying')
+            if $reject;
+    }
+
+    # attach the original encrypted message
+    $args{'Message'}->attach(
+        Type        => 'application/x-rt-original-message',
+        Disposition => 'inline',
+        Data        => ${ $args{'RawMessageRef'} },
+    );
+
+    my @found;
+    foreach my $part ( $args{'Message'}->parts_DFS ) {
+        my $decrypted;
+
+        foreach my $protocol ( @check_protocols ) {
+            my $status = $part->head->get( "X-RT-$protocol-Status" );
+            next unless $status;
+
+            push @found, $protocol;
+
+            for ( RT::Crypt->ParseStatus( Protocol => $protocol, Status => $status ) ) {
+                if ( $_->{Operation} eq 'Decrypt' && $_->{Status} eq 'DONE' ) {
+                    $decrypted = 1;
+                }
+                if ( $_->{Operation} eq 'Verify' && $_->{Status} eq 'DONE' ) {
+                    $part->head->add(
+                        'X-RT-Incoming-Signature' => $_->{UserString}
+                    );
+                }
+            }
+        }
+
+        $part->head->add(
+            'X-RT-Incoming-Encryption' => 
+                $decrypted ? 'Success' : 'Not encrypted'
+        );
+    }
+
+    my %seen;
+    $args{'Message'}->head->add( 'X-RT-Privacy' => $_ )
+        foreach grep !$seen{$_}++, @found;
+
+    return 1;
+}
+
+sub HandleErrors {
+    my %args = (
+        Message => undef,
+        Result => [],
+        @_
+    );
+
+    my $reject = 0;
+
+    my %sent_once = ();
+    foreach my $run ( @{ $args{'Result'} } ) {
+        my @status = RT::Crypt->ParseStatus( Protocol => $run->{'Protocol'}, Status => $run->{'status'} );
+        unless ( $sent_once{'NoPrivateKey'} ) {
+            unless ( CheckNoPrivateKey( Message => $args{'Message'}, Status => \@status ) ) {
+                $sent_once{'NoPrivateKey'}++;
+                $reject = 1 if RT->Config->Get('GnuPG')->{'RejectOnMissingPrivateKey'};
+            }
+        }
+        unless ( $sent_once{'BadData'} ) {
+            unless ( CheckBadData( Message => $args{'Message'}, Status => \@status ) ) {
+                $sent_once{'BadData'}++;
+                $reject = 1 if RT->Config->Get('GnuPG')->{'RejectOnBadData'};
+            }
+        }
+    }
+    return $reject;
+}
+
+sub CheckNoPrivateKey {
+    my %args = (Message => undef, Status => [], @_ );
+    my @status = @{ $args{'Status'} };
+
+    my @decrypts = grep $_->{'Operation'} eq 'Decrypt', @status;
+    return 1 unless @decrypts;
+    foreach my $action ( @decrypts ) {
+        # if at least one secrete key exist then it's another error
+        return 1 if
+            grep !$_->{'User'}{'SecretKeyMissing'},
+                @{ $action->{'EncryptedTo'} };
+    }
+
+    $RT::Logger->error("Couldn't decrypt a message: have no private key");
+
+    my $address = (RT::Interface::Email::ParseSenderAddressFromHead( $args{'Message'}->head ))[0];
+    my ($status) = RT::Interface::Email::SendEmailUsingTemplate(
+        To        => $address,
+        Template  => 'Error: no private key',
+        Arguments => {
+            Message   => $args{'Message'},
+            TicketObj => $args{'Ticket'},
+        },
+        InReplyTo => $args{'Message'},
+    );
+    unless ( $status ) {
+        $RT::Logger->error("Couldn't send 'Error: no private key'");
+    }
+    return 0;
+}
+
+sub CheckBadData {
+    my %args = (Message => undef, Status => [], @_ );
+    my @bad_data_messages = 
+        map $_->{'Message'},
+        grep $_->{'Status'} ne 'DONE' && $_->{'Operation'} eq 'Data',
+        @{ $args{'Status'} };
+    return 1 unless @bad_data_messages;
+
+    $RT::Logger->error("Couldn't process a message: ". join ', ', @bad_data_messages );
+
+    my $address = (RT::Interface::Email::ParseSenderAddressFromHead( $args{'Message'}->head ))[0];
+    my ($status) = RT::Interface::Email::SendEmailUsingTemplate(
+        To        => $address,
+        Template  => 'Error: bad GnuPG data',
+        Arguments => {
+            Messages  => [ @bad_data_messages ],
+            TicketObj => $args{'Ticket'},
+        },
+        InReplyTo => $args{'Message'},
+    );
+    unless ( $status ) {
+        $RT::Logger->error("Couldn't send 'Error: bad GnuPG data'");
+    }
+    return 0;
+}
+
+sub VerifyDecrypt {
+    my %args = (
+        Entity => undef,
+        @_
+    );
+
+    my @res = RT::Crypt->VerifyDecrypt( %args );
+    unless ( @res ) {
+        $RT::Logger->debug("No more encrypted/signed parts");
+        return 1;
+    }
+
+    $RT::Logger->debug('Found encrypted/signed parts');
+
+    # return on any error
+    if ( grep $_->{'exit_code'}, @res ) {
+        $RT::Logger->debug("Error during verify/decrypt operation");
+        return (0, @res);
+    }
+
+    # nesting
+    my ($status, @nested) = VerifyDecrypt( %args );
+    return $status, @res, @nested;
+}
+
+eval "require RT::Interface::Email::Auth::Crypt_Vendor";
+die $@
+  if ( $@
+    && $@ !~ qr{^Can't locate RT/Interface/Email/Auth/Crypt_Vendor.pm} );
+eval "require RT::Interface::Email::Auth::Crypt_Local";
+die $@
+  if ( $@
+    && $@ !~ qr{^Can't locate RT/Interface/Email/Auth/Crypt_Local.pm} );
+
+1;

commit 4159f0ce6abf0257eb463560ebefcc0c65d71444
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Mon Jan 18 17:34:57 2010 +0300

    shrink lib/RT/Interface/Email/Auth/Crypt.pm to shortcut

diff --git a/lib/RT/Interface/Email/Auth/GnuPG.pm b/lib/RT/Interface/Email/Auth/GnuPG.pm
index f0fe2c9..866a870 100644
--- a/lib/RT/Interface/Email/Auth/GnuPG.pm
+++ b/lib/RT/Interface/Email/Auth/GnuPG.pm
@@ -59,196 +59,15 @@ Set up a GnuPG key directory with a pubring containing only the keys
 you care about and specify the following in your SiteConfig.pm
 
     Set(%GnuPGOptions, homedir => '/opt/rt3/var/data/GnuPG');
-    Set(@MailPlugins, 'Auth::MailFrom', 'Auth::GnuPG', ...other filter...);
+    Set(@MailPlugins, 'Auth::MailFrom', 'Auth::Crypt', ...other filter...);
 
 =cut
 
 sub ApplyBeforeDecode { return 1 }
 
-use RT::Crypt::GnuPG;
-use RT::EmailParser ();
+use RT::Interface::Email::Auth::Crypt;
 
-sub GetCurrentUser {
-    my %args = (
-        Message       => undef,
-        RawMessageRef => undef,
-        @_
-    );
-
-    foreach my $p ( $args{'Message'}->parts_DFS ) {
-        $p->head->delete($_) for qw(
-            X-RT-GnuPG-Status X-RT-Incoming-Encrypton
-            X-RT-Incoming-Signature X-RT-Privacy
-        );
-    }
-
-    my $msg = $args{'Message'}->dup;
-
-    my ($status, @res) = VerifyDecrypt(
-        Entity => $args{'Message'}, AddStatus => 1,
-    );
-    if ( $status && !@res ) {
-        $args{'Message'}->head->add(
-            'X-RT-Incoming-Encryption' => 'Not encrypted'
-        );
-
-        return 1;
-    }
-
-    # FIXME: Check if the message is encrypted to the address of
-    # _this_ queue. send rejecting mail otherwise.
-
-    unless ( $status ) {
-        $RT::Logger->error("Had a problem during decrypting and verifying");
-        my $reject = HandleErrors( Message => $args{'Message'}, Result => \@res );
-        return (0, 'rejected because of problems during decrypting and verifying')
-            if $reject;
-    }
-
-    # attach the original encrypted message
-    $args{'Message'}->attach(
-        Type        => 'application/x-rt-original-message',
-        Disposition => 'inline',
-        Data        => ${ $args{'RawMessageRef'} },
-    );
-
-    $args{'Message'}->head->add( 'X-RT-Privacy' => 'PGP' );
-
-    foreach my $part ( $args{'Message'}->parts_DFS ) {
-        my $decrypted;
-
-        my $status = $part->head->get( 'X-RT-GnuPG-Status' );
-        if ( $status ) {
-            for ( RT::Crypt::GnuPG::ParseStatus( $status ) ) {
-                if ( $_->{Operation} eq 'Decrypt' && $_->{Status} eq 'DONE' ) {
-                    $decrypted = 1;
-                }
-                if ( $_->{Operation} eq 'Verify' && $_->{Status} eq 'DONE' ) {
-                    $part->head->add(
-                        'X-RT-Incoming-Signature' => $_->{UserString}
-                    );
-                }
-            }
-        }
-
-        $part->head->add(
-            'X-RT-Incoming-Encryption' => 
-                $decrypted ? 'Success' : 'Not encrypted'
-        );
-    }
-
-    return 1;
-}
-
-sub HandleErrors {
-    my %args = (
-        Message => undef,
-        Result => [],
-        @_
-    );
-
-    my $reject = 0;
-
-    my %sent_once = ();
-    foreach my $run ( @{ $args{'Result'} } ) {
-        my @status = RT::Crypt::GnuPG::ParseStatus( $run->{'status'} );
-        unless ( $sent_once{'NoPrivateKey'} ) {
-            unless ( CheckNoPrivateKey( Message => $args{'Message'}, Status => \@status ) ) {
-                $sent_once{'NoPrivateKey'}++;
-                $reject = 1 if RT->Config->Get('GnuPG')->{'RejectOnMissingPrivateKey'};
-            }
-        }
-        unless ( $sent_once{'BadData'} ) {
-            unless ( CheckBadData( Message => $args{'Message'}, Status => \@status ) ) {
-                $sent_once{'BadData'}++;
-                $reject = 1 if RT->Config->Get('GnuPG')->{'RejectOnBadData'};
-            }
-        }
-    }
-    return $reject;
-}
-
-sub CheckNoPrivateKey {
-    my %args = (Message => undef, Status => [], @_ );
-    my @status = @{ $args{'Status'} };
-
-    my @decrypts = grep $_->{'Operation'} eq 'Decrypt', @status;
-    return 1 unless @decrypts;
-    foreach my $action ( @decrypts ) {
-        # if at least one secrete key exist then it's another error
-        return 1 if
-            grep !$_->{'User'}{'SecretKeyMissing'},
-                @{ $action->{'EncryptedTo'} };
-    }
-
-    $RT::Logger->error("Couldn't decrypt a message: have no private key");
-
-    my $address = (RT::Interface::Email::ParseSenderAddressFromHead( $args{'Message'}->head ))[0];
-    my ($status) = RT::Interface::Email::SendEmailUsingTemplate(
-        To        => $address,
-        Template  => 'Error: no private key',
-        Arguments => {
-            Message   => $args{'Message'},
-            TicketObj => $args{'Ticket'},
-        },
-        InReplyTo => $args{'Message'},
-    );
-    unless ( $status ) {
-        $RT::Logger->error("Couldn't send 'Error: no private key'");
-    }
-    return 0;
-}
-
-sub CheckBadData {
-    my %args = (Message => undef, Status => [], @_ );
-    my @bad_data_messages = 
-        map $_->{'Message'},
-        grep $_->{'Status'} ne 'DONE' && $_->{'Operation'} eq 'Data',
-        @{ $args{'Status'} };
-    return 1 unless @bad_data_messages;
-
-    $RT::Logger->error("Couldn't process a message: ". join ', ', @bad_data_messages );
-
-    my $address = (RT::Interface::Email::ParseSenderAddressFromHead( $args{'Message'}->head ))[0];
-    my ($status) = RT::Interface::Email::SendEmailUsingTemplate(
-        To        => $address,
-        Template  => 'Error: bad GnuPG data',
-        Arguments => {
-            Messages  => [ @bad_data_messages ],
-            TicketObj => $args{'Ticket'},
-        },
-        InReplyTo => $args{'Message'},
-    );
-    unless ( $status ) {
-        $RT::Logger->error("Couldn't send 'Error: bad GnuPG data'");
-    }
-    return 0;
-}
-
-sub VerifyDecrypt {
-    my %args = (
-        Entity => undef,
-        @_
-    );
-
-    my @res = RT::Crypt::GnuPG::VerifyDecrypt( %args );
-    unless ( @res ) {
-        $RT::Logger->debug("No more encrypted/signed parts");
-        return 1;
-    }
-
-    $RT::Logger->debug('Found GnuPG protected parts');
-
-    # return on any error
-    if ( grep $_->{'exit_code'}, @res ) {
-        $RT::Logger->debug("Error during verify/decrypt operation");
-        return (0, @res);
-    }
-
-    # nesting
-    my ($status, @nested) = VerifyDecrypt( %args );
-    return $status, @res, @nested;
-}
+sub GetCurrentUser { return RT::Interface::Email::Auth::Crypt::GetCurrentUser(@_) }
 
 eval "require RT::Interface::Email::Auth::GnuPG_Vendor";
 die $@

commit 0ac17487e40290275d67d49aab47c7152cf66c71
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Mon Jan 18 17:37:13 2010 +0300

    abstracting and maintaining currently used protocol

diff --git a/lib/RT/Crypt.pm b/lib/RT/Crypt.pm
index b086333..071e24c 100644
--- a/lib/RT/Crypt.pm
+++ b/lib/RT/Crypt.pm
@@ -7,6 +7,12 @@ package RT::Crypt;
 require RT::Crypt::GnuPG;
 require RT::Crypt::SMIME;
 
+our @PROTOCOLS = ('GnuPG', 'SMIME');
+
+sub Protocols {
+    return @PROTOCOLS;
+}
+
 sub EnabledOnIncoming {
     return 'GnuPG', 'SMIME';
 }
@@ -17,8 +23,9 @@ sub EnabledOnIncoming {
 sub FindProtectedParts {
     my $self = shift;
     my %args = (
-        Entity => undef,
-        Skip => {},
+        Entity    => undef,
+        Protocols => undef,
+        Skip      => {},
         Scattered => 1,
         @_
     );
@@ -26,7 +33,9 @@ sub FindProtectedParts {
     my $entity = $args{'Entity'};
     return () if $args{'Skip'}{ $entity };
 
-    my @protocols = $self->EnabledOnIncoming;
+    my @protocols = $args{'Protocols'}
+        ? @{ $args{'Protocols'} } 
+        : $self->EnabledOnIncoming;
 
     foreach my $protocol ( @protocols ) {
         my $class = 'RT::Crypt::'. $protocol;
@@ -83,7 +92,7 @@ sub SignEncrypt {
         ];
     }
 
-    my $using = delete $args{'Using'} || 'GnuPG';
+    my $using = delete $args{'Protocol'} || 'GnuPG';
     my $class = 'RT::Crypt::'. $using;
 
     return $class->SignEncrypt( %args );
@@ -106,6 +115,7 @@ sub VerifyDecrypt {
         my $protocol = $protected->{'Protocol'};
         my $class = 'RT::Crypt::'. $protocol;
         my %res = $class->VerifyDecrypt( %args, %$protected );
+        $res{'Protocol'} = $protocol;
         push @res, \%res;
     }
     return @res;

commit 16be52767702036b0f7f8809067b3e81b55cb61c
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Mon Jan 18 17:38:08 2010 +0300

    ParseStatus in RT::Crypt

diff --git a/lib/RT/Crypt.pm b/lib/RT/Crypt.pm
index 071e24c..b3c393d 100644
--- a/lib/RT/Crypt.pm
+++ b/lib/RT/Crypt.pm
@@ -121,4 +121,15 @@ sub VerifyDecrypt {
     return @res;
 }
 
+sub ParseStatus {
+    my $self = shift;
+    my %args = (
+        Protocol => undef,
+        Status   => '',
+        @_
+    );
+    my $class = 'RT::Crypt::'. $args{'Protocol'};
+    return $class->ParseStatus( $args{'Status'} );
+}
+
 1;

commit 5318e3ebec17051933f3361d610141c212eb5065
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Mon Jan 18 17:47:03 2010 +0300

    more on methods instead of functions

diff --git a/lib/RT/Attachment_Overlay.pm b/lib/RT/Attachment_Overlay.pm
index e709814..d3b460c 100644
--- a/lib/RT/Attachment_Overlay.pm
+++ b/lib/RT/Attachment_Overlay.pm
@@ -676,7 +676,7 @@ sub Encrypt {
     $self->SetHeader( 'Content-Type' => $type );
 
     my $content = $self->Content;
-    my %res = RT::Crypt::GnuPG::SignEncryptContent(
+    my %res = RT::Crypt->SignEncryptContent(
         Content => \$content,
         Sign => 0,
         Encrypt => 1,
diff --git a/lib/RT/Crypt/GnuPG.pm b/lib/RT/Crypt/GnuPG.pm
index a6fc17f..26dd6db 100644
--- a/lib/RT/Crypt/GnuPG.pm
+++ b/lib/RT/Crypt/GnuPG.pm
@@ -1689,6 +1689,7 @@ my %ignore_keyword = map { $_ => 1 } qw(
 );
 
 sub ParseStatus {
+    my $self = shift;
     my $status = shift;
     return () unless $status;
 
diff --git a/lib/RT/Interface/Email.pm b/lib/RT/Interface/Email.pm
index 9a5f59d..4283df4 100755
--- a/lib/RT/Interface/Email.pm
+++ b/lib/RT/Interface/Email.pm
@@ -735,8 +735,8 @@ sub SignEncrypt {
     $RT::Logger->debug("$msgid Signing message") if $args{'Sign'};
     $RT::Logger->debug("$msgid Encrypting message") if $args{'Encrypt'};
 
-    require RT::Crypt::GnuPG;
-    my %res = RT::Crypt::GnuPG::SignEncrypt( %args );
+    require RT::Crypt;
+    my %res = RT::Crypt->SignEncrypt( %args );
     return 1 unless $res{'exit_code'};
 
     my @status = RT::Crypt::GnuPG::ParseStatus( $res{'status'} );
@@ -803,7 +803,7 @@ sub SignEncrypt {
     }
 
     # redo without broken recipients
-    %res = RT::Crypt::GnuPG::SignEncrypt( %args );
+    %res = RT::Crypt->SignEncrypt( %args );
     return 0 if $res{'exit_code'};
 
     return 1;
diff --git a/t/mail/crypt-gnupg.t b/t/mail/crypt-gnupg.t
index f33fbab..42d218f 100644
--- a/t/mail/crypt-gnupg.t
+++ b/t/mail/crypt-gnupg.t
@@ -38,7 +38,7 @@ diag 'only signing. correct passphrase' if $ENV{'TEST_VERBOSE'};
         Subject => 'test',
         Data    => ['test'],
     );
-    my %res = RT::Crypt::GnuPG::SignEncrypt( Entity => $entity, Encrypt => 0, Passphrase => 'test' );
+    my %res = RT::Crypt->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'} );
@@ -52,7 +52,7 @@ diag 'only signing. correct passphrase' if $ENV{'TEST_VERBOSE'};
     ok( $entity->is_multipart, 'signed message is multipart' );
     is( $entity->parts, 2, 'two parts' );
 
-    my @parts = RT::Crypt::GnuPG::FindProtectedParts( Entity => $entity );
+    my @parts = RT::Crypt->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" );
@@ -74,7 +74,7 @@ diag 'only signing. missing passphrase' if $ENV{'TEST_VERBOSE'};
         Subject => 'test',
         Data    => ['test'],
     );
-    my %res = RT::Crypt::GnuPG::SignEncrypt( Entity => $entity, Encrypt => 0, Passphrase => '' );
+    my %res = RT::Crypt->SignEncrypt( Entity => $entity, Encrypt => 0, Passphrase => '' );
     ok( $res{'exit_code'}, "couldn't sign without passphrase");
     ok( $res{'error'} || $res{'logger'}, "error is here" );
 
@@ -91,7 +91,7 @@ diag 'only signing. wrong passphrase' if $ENV{'TEST_VERBOSE'};
         Subject => 'test',
         Data    => ['test'],
     );
-    my %res = RT::Crypt::GnuPG::SignEncrypt( Entity => $entity, Encrypt => 0, Passphrase => 'wrong' );
+    my %res = RT::Crypt->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" );
 
@@ -109,7 +109,7 @@ diag 'encryption only' if $ENV{'TEST_VERBOSE'};
         Subject => 'test',
         Data    => ['test'],
     );
-    my %res = RT::Crypt::GnuPG::SignEncrypt( Entity => $entity, Sign => 0 );
+    my %res = RT::Crypt->SignEncrypt( Entity => $entity, Sign => 0 );
     ok( !$res{'exit_code'}, "successful encryption" );
     ok( !$res{'logger'}, "no records in logger" );
 
@@ -120,7 +120,7 @@ diag 'encryption only' if $ENV{'TEST_VERBOSE'};
 
     ok($entity, 'get an encrypted part');
 
-    my @parts = RT::Crypt::GnuPG::FindProtectedParts( Entity => $entity );
+    my @parts = RT::Crypt->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" );
@@ -135,7 +135,7 @@ diag 'encryption only, bad recipient' if $ENV{'TEST_VERBOSE'};
         Subject => 'test',
         Data    => ['test'],
     );
-    my %res = RT::Crypt::GnuPG::SignEncrypt( Entity => $entity, Sign => 0 );
+    my %res = RT::Crypt->SignEncrypt( Entity => $entity, Sign => 0 );
     ok( $res{'exit_code'}, 'no way to encrypt without keys of recipients');
     ok( $res{'logger'}, "errors are in logger" );
 
@@ -152,7 +152,7 @@ diag 'encryption and signing with combined method' if $ENV{'TEST_VERBOSE'};
         Subject => 'test',
         Data    => ['test'],
     );
-    my %res = RT::Crypt::GnuPG::SignEncrypt( Entity => $entity, Passphrase => 'test' );
+    my %res = RT::Crypt->SignEncrypt( Entity => $entity, Passphrase => 'test' );
     ok( !$res{'exit_code'}, "successful encryption with signing" );
     ok( !$res{'logger'}, "no records in logger" );
 
@@ -167,7 +167,7 @@ diag 'encryption and signing with combined method' if $ENV{'TEST_VERBOSE'};
 
     ok($entity, 'get an encrypted and signed part');
 
-    my @parts = RT::Crypt::GnuPG::FindProtectedParts( Entity => $entity );
+    my @parts = RT::Crypt->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" );
@@ -182,14 +182,14 @@ diag 'encryption and signing with cascading, sign on encrypted' if $ENV{'TEST_VE
         Subject => 'test',
         Data    => ['test'],
     );
-    my %res = RT::Crypt::GnuPG::SignEncrypt( Entity => $entity, Sign => 0 );
+    my %res = RT::Crypt->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' );
+    %res = RT::Crypt->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 );
+    my @parts = RT::Crypt->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" );
@@ -204,7 +204,7 @@ diag 'find signed/encrypted part deep inside' if $ENV{'TEST_VERBOSE'};
         Subject => 'test',
         Data    => ['test'],
     );
-    my %res = RT::Crypt::GnuPG::SignEncrypt( Entity => $entity, Sign => 0 );
+    my %res = RT::Crypt->SignEncrypt( Entity => $entity, Sign => 0 );
     ok( !$res{'exit_code'}, "success" );
     $entity->make_multipart( 'mixed', Force => 1 );
     $entity->attach(
@@ -212,7 +212,7 @@ diag 'find signed/encrypted part deep inside' if $ENV{'TEST_VERBOSE'};
         Data => ['-'x76, 'this is mailing list'],
     );
 
-    my @parts = RT::Crypt::GnuPG::FindProtectedParts( Entity => $entity );
+    my @parts = RT::Crypt->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" );
@@ -227,11 +227,11 @@ diag 'wrong signed/encrypted parts: no protocol' if $ENV{'TEST_VERBOSE'};
         Subject => 'test',
         Data    => ['test'],
     );
-    my %res = RT::Crypt::GnuPG::SignEncrypt( Entity => $entity, Sign => 0 );
+    my %res = RT::Crypt->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 );
+    my @parts = RT::Crypt->FindProtectedParts( Entity => $entity );
     is( scalar @parts, 0, 'no protected parts' );
 }
 
@@ -243,11 +243,11 @@ diag 'wrong signed/encrypted parts: not enought parts' if $ENV{'TEST_VERBOSE'};
         Subject => 'test',
         Data    => ['test'],
     );
-    my %res = RT::Crypt::GnuPG::SignEncrypt( Entity => $entity, Sign => 0 );
+    my %res = RT::Crypt->SignEncrypt( Entity => $entity, Sign => 0 );
     ok( !$res{'exit_code'}, 'success' );
     $entity->parts([ $entity->parts(0) ]);
 
-    my @parts = RT::Crypt::GnuPG::FindProtectedParts( Entity => $entity );
+    my @parts = RT::Crypt->FindProtectedParts( Entity => $entity );
     is( scalar @parts, 0, 'no protected parts' );
 }
 
@@ -259,11 +259,11 @@ diag 'wrong signed/encrypted parts: wrong proto' if $ENV{'TEST_VERBOSE'};
         Subject => 'test',
         Data    => ['test'],
     );
-    my %res = RT::Crypt::GnuPG::SignEncrypt( Entity => $entity, Sign => 0 );
+    my %res = RT::Crypt->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 );
+    my @parts = RT::Crypt->FindProtectedParts( Entity => $entity );
     is( scalar @parts, 0, 'no protected parts' );
 }
 
@@ -275,11 +275,11 @@ diag 'wrong signed/encrypted parts: wrong proto' if $ENV{'TEST_VERBOSE'};
         Subject => 'test',
         Data    => ['test'],
     );
-    my %res = RT::Crypt::GnuPG::SignEncrypt( Entity => $entity, Encrypt => 0, Passphrase => 'test' );
+    my %res = RT::Crypt->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 );
+    my @parts = RT::Crypt->FindProtectedParts( Entity => $entity );
     is( scalar @parts, 0, 'no protected parts' );
 }
 
@@ -289,7 +289,7 @@ diag 'verify inline and in attachment signatures' if $ENV{'TEST_VERBOSE'};
     my $parser = new MIME::Parser;
     my $entity = $parser->parse( $fh );
 
-    my @parts = RT::Crypt::GnuPG::FindProtectedParts( Entity => $entity );
+    my @parts = RT::Crypt->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" );

commit 079be6f9674aad30af329eb6323b4a038b9594c2
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Thu Jan 21 12:05:08 2010 +0300

    more conversions from funstions to class methods

diff --git a/lib/RT/Crypt/GnuPG.pm b/lib/RT/Crypt/GnuPG.pm
index 26dd6db..314abfb 100644
--- a/lib/RT/Crypt/GnuPG.pm
+++ b/lib/RT/Crypt/GnuPG.pm
@@ -438,7 +438,7 @@ sub SignEncryptRFC3156 {
     my $entity = $args{'Entity'};
 
     if ( $args{'Sign'} && !defined $args{'Passphrase'} ) {
-        $args{'Passphrase'} = GetPassphrase( Address => $args{'Signer'} );
+        $args{'Passphrase'} = $self->GetPassphrase( Address => $args{'Signer'} );
     }
 
     my %res;
@@ -620,7 +620,7 @@ sub _SignEncryptTextInline {
     );
 
     if ( $args{'Sign'} && !defined $args{'Passphrase'} ) {
-        $args{'Passphrase'} = GetPassphrase( Address => $args{'Signer'} );
+        $args{'Passphrase'} = $self->GetPassphrase( Address => $args{'Signer'} );
     }
 
     if ( $args{'Encrypt'} ) {
@@ -709,7 +709,7 @@ sub _SignEncryptAttachmentInline {
     );
 
     if ( $args{'Sign'} && !defined $args{'Passphrase'} ) {
-        $args{'Passphrase'} = GetPassphrase( Address => $args{'Signer'} );
+        $args{'Passphrase'} = $self->GetPassphrase( Address => $args{'Signer'} );
     }
 
     my $entity = $args{'Entity'};
@@ -812,7 +812,7 @@ sub SignEncryptContent {
     );
 
     if ( $args{'Sign'} && !defined $args{'Passphrase'} ) {
-        $args{'Passphrase'} = GetPassphrase( Address => $args{'Signer'} );
+        $args{'Passphrase'} = $self->GetPassphrase( Address => $args{'Signer'} );
     }
 
     if ( $args{'Encrypt'} ) {
@@ -886,7 +886,7 @@ sub CheckIfProtected {
 
     # RFC3156, multipart/{signed,encrypted}
     my $type = $entity->effective_type;
-    return () unless $type = =~ /^multipart\/(?:encrypted|signed)$/;
+    return () unless $type =~ /^multipart\/(?:encrypted|signed)$/;
 
     unless ( $entity->parts == 2 ) {
         $RT::Logger->error( "Encrypted or signed entity must has two subparts. Skipped" );
@@ -1247,7 +1247,7 @@ sub DecryptRFC3156 {
         RT::EmailParser->_DecodeBody($args{'Data'});
     }
 
-    $args{'Passphrase'} = GetPassphrase()
+    $args{'Passphrase'} = $self->GetPassphrase()
         unless defined $args{'Passphrase'};
 
     my ($tmp_fh, $tmp_fn) = File::Temp::tempfile( UNLINK => 1 );
@@ -1326,7 +1326,7 @@ sub DecryptInline {
         RT::EmailParser->_DecodeBody($args{'Data'});
     }
 
-    $args{'Passphrase'} = GetPassphrase()
+    $args{'Passphrase'} = $self->GetPassphrase()
         unless defined $args{'Passphrase'};
 
     my ($tmp_fh, $tmp_fn) = File::Temp::tempfile( UNLINK => 1 );
@@ -1466,7 +1466,7 @@ sub DecryptAttachment {
         RT::EmailParser->_DecodeBody($args{'Data'});
     }
 
-    $args{'Passphrase'} = GetPassphrase()
+    $args{'Passphrase'} = $self->GetPassphrase()
         unless defined $args{'Passphrase'};
 
     my ($tmp_fh, $tmp_fn) = File::Temp::tempfile( UNLINK => 1 );
@@ -1513,7 +1513,7 @@ sub DecryptContent {
         meta_interactive => 0,
     );
 
-    $args{'Passphrase'} = GetPassphrase()
+    $args{'Passphrase'} = $self->GetPassphrase()
         unless defined $args{'Passphrase'};
 
     my ($tmp_fh, $tmp_fn) = File::Temp::tempfile( UNLINK => 1 );
@@ -1579,6 +1579,7 @@ Returns passphrase, called whenever it's required with Address as a named argume
 =cut
 
 sub GetPassphrase {
+    my $self = shift;
     my %args = ( Address => undef, @_ );
     return 'test';
 }
@@ -1978,7 +1979,7 @@ also listed.
 
 sub GetKeysForEncryption {
     my $key_id = shift;
-    my %res = GetKeysInfo( $key_id, 'public', @_ );
+    my %res = $self->GetKeysInfo( $key_id, 'public', @_ );
     return %res if $res{'exit_code'};
     return %res unless $res{'info'};
 
@@ -1999,7 +2000,7 @@ sub GetKeysForEncryption {
 
 sub GetKeysForSigning {
     my $key_id = shift;
-    return GetKeysInfo( $key_id, 'private', @_ );
+    return $self->GetKeysInfo( $key_id, 'private', @_ );
 }
 
 sub CheckRecipients {
@@ -2076,12 +2077,13 @@ sub GetPrivateKeyInfo {
 }
 
 sub GetKeyInfo {
-    my %res = GetKeysInfo(@_);
+    my %res = $self->GetKeysInfo(@_);
     $res{'info'} = $res{'info'}->[0];
     return %res;
 }
 
 sub GetKeysInfo {
+    my $self = shift;
     my $email = shift;
     my $type = shift || 'public';
     my $force = shift;
@@ -2287,6 +2289,7 @@ sub _ParseDate {
 }
 
 sub DeleteKey {
+    my $self = shift;
     my $key = shift;
 
     my $gnupg = new GnuPG::Interface;
@@ -2335,6 +2338,7 @@ sub DeleteKey {
 }
 
 sub ImportKey {
+    my $self = shift;
     my $key = shift;
 
     my $gnupg = new GnuPG::Interface;
@@ -2388,6 +2392,7 @@ Returns a true value if all went well.
 =cut
 
 sub DrySign {
+    my $self = shift;
     my $from = shift;
 
     my $mime = MIME::Entity->build(
@@ -2420,6 +2425,7 @@ properly (and false otherwise).
 
 
 sub Probe {
+    my $self = shift;
     my $gnupg = new GnuPG::Interface;
     my %opt = RT->Config->Get('GnuPGOptions');
     $gnupg->options->hash_init(

commit 1b8ed363b25c440e53cdc1b39934bd1f1746a007
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Thu Jan 21 12:06:05 2010 +0300

    when we SignEncrypt store protocol in results

diff --git a/lib/RT/Crypt.pm b/lib/RT/Crypt.pm
index b3c393d..8b8a6e2 100644
--- a/lib/RT/Crypt.pm
+++ b/lib/RT/Crypt.pm
@@ -92,10 +92,12 @@ sub SignEncrypt {
         ];
     }
 
-    my $using = delete $args{'Protocol'} || 'GnuPG';
-    my $class = 'RT::Crypt::'. $using;
+    my $protocol = delete $args{'Protocol'} || 'GnuPG';
+    my $class = 'RT::Crypt::'. $protocol;
 
-    return $class->SignEncrypt( %args );
+    my %res = $class->SignEncrypt( %args );
+    $res{'Protocol'} = $protocol;
+    return %res;
 }
 
 sub VerifyDecrypt {

commit 410287325ccdcfab05956fd364391290488c11b4
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Thu Jan 21 12:08:15 2010 +0300

    switch to using class methods, for example generic ParseStatus

diff --git a/lib/RT/Attachment_Overlay.pm b/lib/RT/Attachment_Overlay.pm
index d3b460c..6a97966 100644
--- a/lib/RT/Attachment_Overlay.pm
+++ b/lib/RT/Attachment_Overlay.pm
@@ -662,7 +662,7 @@ sub Encrypt {
         RT->Config->Get('CorrespondAddress'),
         RT->Config->Get('CommentAddress'),
     ) {
-        my %res = RT::Crypt::GnuPG::GetKeysInfo( $address, 'private' );
+        my %res = RT::Crypt::GnuPG->GetKeysInfo( $address, 'private' );
         next if $res{'exit_code'} || !$res{'info'};
         %res = RT::Crypt::GnuPG::GetKeysForEncryption( $address );
         next if $res{'exit_code'} || !$res{'info'};
diff --git a/lib/RT/Interface/Email.pm b/lib/RT/Interface/Email.pm
index 4283df4..55ceddb 100755
--- a/lib/RT/Interface/Email.pm
+++ b/lib/RT/Interface/Email.pm
@@ -739,7 +739,9 @@ sub SignEncrypt {
     my %res = RT::Crypt->SignEncrypt( %args );
     return 1 unless $res{'exit_code'};
 
-    my @status = RT::Crypt::GnuPG::ParseStatus( $res{'status'} );
+    my @status = RT::Crypt->ParseStatus(
+        Protocol => $res{'Protocol'}, Status => $res{'status'},
+    );
 
     my @bad_recipients;
     foreach my $line ( @status ) {
diff --git a/lib/RT/Test.pm b/lib/RT/Test.pm
index 5615324..62e84e9 100644
--- a/lib/RT/Test.pm
+++ b/lib/RT/Test.pm
@@ -852,7 +852,7 @@ sub import_gnupg_key {
     die "can't find the dir where gnupg keys are stored"
       unless $abs_path;
 
-    return RT::Crypt::GnuPG::ImportKey(
+    return RT::Crypt::GnuPG->ImportKey(
         RT::Test->file_content( [ $abs_path, $key ] ) );
 }
 

commit 7f61fb4a3a540d8540281dce59221179c12b361b
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Thu Jan 21 12:28:59 2010 +0300

    convert t/mail/crypt-gnupg.t over new API

diff --git a/t/mail/crypt-gnupg.t b/t/mail/crypt-gnupg.t
index 42d218f..b05a903 100644
--- a/t/mail/crypt-gnupg.t
+++ b/t/mail/crypt-gnupg.t
@@ -41,7 +41,9 @@ diag 'only signing. correct passphrase' if $ENV{'TEST_VERBOSE'};
     my %res = RT::Crypt->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'} );
+    my @status = RT::Crypt->ParseStatus(
+        Protocol => $res{'Protocol'}, Status => $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');
@@ -58,9 +60,11 @@ diag 'only signing. correct passphrase' if $ENV{'TEST_VERBOSE'};
     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 );
+    my @res = RT::Crypt->VerifyDecrypt( Entity => $entity );
     is scalar @res, 1, 'one operation';
-    @status = RT::Crypt::GnuPG::ParseStatus( $res[0]{'status'} );
+    @status = RT::Crypt->ParseStatus(
+        Protocol => $res[0]{'Protocol'}, Status => $res[0]{'status'}
+    );
     is( scalar @status, 1, 'one record');
     is( $status[0]->{'Operation'}, 'Verify', 'operation is correct');
     is( $status[0]->{'Status'}, 'DONE', 'good passphrase');
@@ -78,7 +82,9 @@ diag 'only signing. missing passphrase' if $ENV{'TEST_VERBOSE'};
     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'} );
+    my @status = RT::Crypt->ParseStatus(
+        Protocol => $res{'Protocol'}, Status => $res{'status'}
+    );
     is( scalar @status, 1, 'one record');
     is( $status[0]->{'Operation'}, 'PassphraseCheck', 'operation is correct');
     is( $status[0]->{'Status'}, 'MISSING', 'missing passphrase');
@@ -95,7 +101,9 @@ diag 'only signing. wrong passphrase' if $ENV{'TEST_VERBOSE'};
     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'} );
+    my @status = RT::Crypt->ParseStatus(
+        Protocol => $res{'Protocol'}, Status => $res{'status'}
+    );
     is( scalar @status, 1, 'one record');
     is( $status[0]->{'Operation'}, 'PassphraseCheck', 'operation is correct');
     is( $status[0]->{'Status'}, 'BAD', 'wrong passphrase');
@@ -113,7 +121,9 @@ diag 'encryption only' if $ENV{'TEST_VERBOSE'};
     ok( !$res{'exit_code'}, "successful encryption" );
     ok( !$res{'logger'}, "no records in logger" );
 
-    my @status = RT::Crypt::GnuPG::ParseStatus( $res{'status'} );
+    my @status = RT::Crypt->ParseStatus(
+        Protocol => $res{'Protocol'}, Status => $res{'status'}
+    );
     is( scalar @status, 1, 'one record');
     is( $status[0]->{'Operation'}, 'Encrypt', 'operation is correct');
     is( $status[0]->{'Status'}, 'DONE', 'done');
@@ -139,7 +149,9 @@ diag 'encryption only, bad recipient' if $ENV{'TEST_VERBOSE'};
     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'} );
+    my @status = RT::Crypt->ParseStatus(
+        Protocol => $res{'Protocol'}, Status => $res{'status'}
+    );
     is( scalar @status, 1, 'one record');
     is( $status[0]->{'Keyword'}, 'INV_RECP', 'invalid recipient');
 }
@@ -156,7 +168,9 @@ diag 'encryption and signing with combined method' if $ENV{'TEST_VERBOSE'};
     ok( !$res{'exit_code'}, "successful encryption with signing" );
     ok( !$res{'logger'}, "no records in logger" );
 
-    my @status = RT::Crypt::GnuPG::ParseStatus( $res{'status'} );
+    my @status = RT::Crypt->ParseStatus(
+        Protocol => $res{'Protocol'}, Status => $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');
@@ -300,8 +314,10 @@ diag 'verify inline and in attachment signatures' if $ENV{'TEST_VERBOSE'};
     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'} );
+    my @res = RT::Crypt->VerifyDecrypt( Entity => $entity );
+    my @status = RT::Crypt->ParseStatus(
+        Protocol => $res[0]{'Protocol'}, Status => $res[0]{'status'}
+    );
     is( scalar @status, 1, 'one record');
     is( $status[0]->{'Operation'}, 'Verify', 'operation is correct');
     is( $status[0]->{'Status'}, 'DONE', 'good passphrase');

commit 4ee62c60b4be4ef3fe991cabed49e45a136455a2
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Thu Jan 21 12:29:32 2010 +0300

    partialy convert ShowGnuPGStatus over new API
    
    re-verification still needs more love

diff --git a/share/html/Ticket/Elements/ShowGnuPGStatus b/share/html/Ticket/Elements/ShowGnuPGStatus
index 08814aa..dc1178a 100644
--- a/share/html/Ticket/Elements/ShowGnuPGStatus
+++ b/share/html/Ticket/Elements/ShowGnuPGStatus
@@ -62,10 +62,12 @@ $Reverify     => 1
 my @runs;
 my $needs_unsigned_warning = $WarnUnsigned;
 
+my @protocols = RT::Crypt->Protocols;
+my $re_protocols = join '|', map "\Q$_\E", @protocols;
+
 foreach ( $Attachment->SplitHeaders ) {
-    if ( s/^X-RT-GnuPG-Status:\s*//i ) {
-        require RT::Crypt::GnuPG;
-        push @runs, [ RT::Crypt::GnuPG::ParseStatus( $_ ) ];
+    if ( s/^X-RT-($re_protocols)-Status:\s*//io ) {
+        push @runs, [ RT::Crypt->ParseStatus( Protocol => "$1", Status => $_ ) ];
     }
 
     $needs_unsigned_warning = 0 if /^X-RT-Incoming-Signature:/;
@@ -103,14 +105,14 @@ my $reverify_cb = sub {
         return (0, "Couldn't parse content of attachment #". $original->id);
     }
 
-    use RT::Interface::Email::Auth::GnuPG;
-    my ($status, @res) = RT::Interface::Email::Auth::GnuPG::VerifyDecrypt( Entity => $entity );
+    use RT::Interface::Email::Auth::Crypt;
+    my ($status, @res) = RT::Interface::Email::Auth::Crypt::VerifyDecrypt( Entity => $entity );
     if ( $status && !@res ) {
         # imposible in this situation
         return (0, "Content of attachment #". $original->id ." is not signed and/or encrypted");
     }
     elsif ( @res ) {
-        require RT::Crypt::GnuPG;
+        require RT::Crypt;
 
         $top->DelHeader('X-RT-GnuPG-Status');
         $top->AddHeader(map { ('X-RT-GnuPG-Status' => $_->{'status'} ) } @res);

commit 487d028870649d2bbd5bdfd2976d8ea36b6bc1df
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Thu Jan 21 12:38:10 2010 +0300

    more on ::ParseStatus ->  ->ParseStatus

diff --git a/share/html/Elements/GnuPG/SignEncryptWidget b/share/html/Elements/GnuPG/SignEncryptWidget
index 9303fb5..a5bdd23 100644
--- a/share/html/Elements/GnuPG/SignEncryptWidget
+++ b/share/html/Elements/GnuPG/SignEncryptWidget
@@ -129,7 +129,7 @@ if ( $self->{'Sign'} ) {
         ? ( $QueueObj->CommentAddress || RT->Config->Get('CommentAddress') )
         : ( $QueueObj->CorrespondAddress || RT->Config->Get('CorrespondAddress') );
 
-    unless ( RT::Crypt::GnuPG::DrySign( $address ) ) {
+    unless ( RT::Crypt::GnuPG->DrySign( $address ) ) {
         push @{ $self->{'GnuPGCanNotSignAs'} ||= [] }, $address;
         $checks_failure = 1;
     } else {
diff --git a/share/html/Ticket/Elements/ShowGnuPGStatus b/share/html/Ticket/Elements/ShowGnuPGStatus
index dc1178a..fa78bc8 100644
--- a/share/html/Ticket/Elements/ShowGnuPGStatus
+++ b/share/html/Ticket/Elements/ShowGnuPGStatus
@@ -119,7 +119,10 @@ my $reverify_cb = sub {
         $top->SetHeader('X-RT-Privacy' => 'PGP' );
         $top->DelHeader('X-RT-Incoming-Signature');
 
-        my @status = RT::Crypt::GnuPG::ParseStatus( $res[0]->{'status'} );
+        my @status = RT::Crypt->ParseStatus(
+            Protocol => $res[0]{'Protocol'},
+            Status => $res[0]{'status'},
+        );
         for ( @status ) {
             if ( $_->{'Operation'} eq 'Verify' && $_->{'Status'} eq 'DONE' ) {
                 $top->AddHeader( 'X-RT-Incoming-Signature' => $_->{'UserString'} );

commit e5406ae4086fd0aad2e7c82fc350ffa0dd999a65
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Thu Jan 21 22:48:38 2010 +0300

    syntax error fixes

diff --git a/lib/RT/Crypt/GnuPG.pm b/lib/RT/Crypt/GnuPG.pm
index 314abfb..554312e 100644
--- a/lib/RT/Crypt/GnuPG.pm
+++ b/lib/RT/Crypt/GnuPG.pm
@@ -406,6 +406,7 @@ sub SignEncrypt {
 }
 
 sub SignEncryptRFC3156 {
+    my $self = shift;
     my %args = (
         Entity => undef,
 
@@ -1010,14 +1011,14 @@ sub FindScatteredParts {
     foreach my $part ( @parts ) {
         next if $args{'Skip'}{$part};
 
-        my $type = $self->_CheckIfProtectedInline( $entity );
+        my $type = $self->_CheckIfProtectedInline( $part );
         next unless $type;
 
         $args{'Skip'}{$part} = 1;
         push @res, {
             Type      => $type,
             Format    => 'Inline',
-            Data      => $entity,
+            Data      => $part,
         };
     }
 
@@ -1085,7 +1086,7 @@ sub VerifyDecrypt {
         if ( $args{'SetStatus'} || $args{'AddStatus'} ) {
             my $method = $args{'AddStatus'} ? 'add' : 'set';
             $status_on->head->$method(
-                'X-RT-GnuPG-Status' => $res[-1]->{'status'}
+                'X-RT-GnuPG-Status' => $res{'status'}
             );
         }
     } elsif ( $item->{'Type'} eq 'encrypted' ) {
@@ -1105,13 +1106,13 @@ sub VerifyDecrypt {
         if ( $args{'SetStatus'} || $args{'AddStatus'} ) {
             my $method = $args{'AddStatus'} ? 'add' : 'set';
             $status_on->head->$method(
-                'X-RT-GnuPG-Status' => $res[-1]->{'status'}
+                'X-RT-GnuPG-Status' => $res{'status'}
             );
         }
     } else {
         die "Unknow type '". $item->{'Type'} ."' of protected item";
     }
-    return @res;
+    return %res;
 }
 
 sub VerifyInline { return (shift)->DecryptInline( @_ ) }
@@ -1978,6 +1979,7 @@ also listed.
 =cut
 
 sub GetKeysForEncryption {
+    my $self = shift;
     my $key_id = shift;
     my %res = $self->GetKeysInfo( $key_id, 'public', @_ );
     return %res if $res{'exit_code'};
@@ -1999,18 +2001,20 @@ sub GetKeysForEncryption {
 }
 
 sub GetKeysForSigning {
+    my $self = shift;
     my $key_id = shift;
     return $self->GetKeysInfo( $key_id, 'private', @_ );
 }
 
 sub CheckRecipients {
+    my $self = shift;
     my @recipients = (@_);
 
     my ($status, @issues) = (1, ());
 
     my %seen;
     foreach my $address ( grep !$seen{ lc $_ }++, map $_->address, @recipients ) {
-        my %res = GetKeysForEncryption( $address );
+        my %res = $self->GetKeysForEncryption( $address );
         if ( $res{'info'} && @{ $res{'info'} } == 1 && $res{'info'}[0]{'TrustLevel'} > 0 ) {
             # good, one suitable and trusted key 
             next;
@@ -2069,14 +2073,15 @@ sub CheckRecipients {
 }
 
 sub GetPublicKeyInfo {
-    return GetKeyInfo( shift, 'public', @_ );
+    return (shift)->GetKeyInfo( shift, 'public', @_ );
 }
 
 sub GetPrivateKeyInfo {
-    return GetKeyInfo( shift, 'private', @_ );
+    return (shift)->GetKeyInfo( shift, 'private', @_ );
 }
 
 sub GetKeyInfo {
+    my $self = shift;
     my %res = $self->GetKeysInfo(@_);
     $res{'info'} = $res{'info'}->[0];
     return %res;

commit 41cb4d8258d31b86149e72e6298d68c96ad7ac52
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Thu Jan 21 22:50:10 2010 +0300

    RT::Crypt->LoadImplementation method

diff --git a/lib/RT/Crypt.pm b/lib/RT/Crypt.pm
index 8b8a6e2..aef988a 100644
--- a/lib/RT/Crypt.pm
+++ b/lib/RT/Crypt.pm
@@ -17,6 +17,15 @@ sub EnabledOnIncoming {
     return 'GnuPG', 'SMIME';
 }
 
+{ my %cache;
+sub LoadImplementation {
+    my $class = 'RT::Crypt::'. $_[1];
+    return $class if $cache{ $class }++;
+
+    eval "require $class; 1" or do { require Carp; Carp::confess( $@ ) };
+    return $class;
+} }
+
 # encryption and signatures can be nested one over another, for example:
 # GPG inline signed text can be signed with SMIME
 
@@ -38,7 +47,7 @@ sub FindProtectedParts {
         : $self->EnabledOnIncoming;
 
     foreach my $protocol ( @protocols ) {
-        my $class = 'RT::Crypt::'. $protocol;
+        my $class = $self->LoadImplementation( $protocol );
         my %info = $class->CheckIfProtected( Entity => $entity );
         next unless keys %info;
 
@@ -93,7 +102,7 @@ sub SignEncrypt {
     }
 
     my $protocol = delete $args{'Protocol'} || 'GnuPG';
-    my $class = 'RT::Crypt::'. $protocol;
+    my $class = $self->LoadImplementation( $protocol );
 
     my %res = $class->SignEncrypt( %args );
     $res{'Protocol'} = $protocol;
@@ -115,8 +124,8 @@ sub VerifyDecrypt {
     my @protected = $self->FindProtectedParts( Entity => $args{'Entity'} );
     foreach my $protected ( @protected ) {
         my $protocol = $protected->{'Protocol'};
-        my $class = 'RT::Crypt::'. $protocol;
-        my %res = $class->VerifyDecrypt( %args, %$protected );
+        my $class = $self->LoadImplementation( $protocol );
+        my %res = $class->VerifyDecrypt( %args, Info => $protected );
         $res{'Protocol'} = $protocol;
         push @res, \%res;
     }
@@ -130,8 +139,7 @@ sub ParseStatus {
         Status   => '',
         @_
     );
-    my $class = 'RT::Crypt::'. $args{'Protocol'};
-    return $class->ParseStatus( $args{'Status'} );
+    return $self->LoadImplementation( $args{'Protocol'} )->ParseStatus( $args{'Status'} );
 }
 
 1;

commit f74e93eba04c148e1d9f9280e7c2a09ca8ef6b8f
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Thu Jan 21 22:50:52 2010 +0300

    don't recurse into multipart/{signed,encrypted} messages

diff --git a/lib/RT/Crypt.pm b/lib/RT/Crypt.pm
index aef988a..9f5eb77 100644
--- a/lib/RT/Crypt.pm
+++ b/lib/RT/Crypt.pm
@@ -56,6 +56,15 @@ sub FindProtectedParts {
         return \%info;
     }
 
+    if ( $entity->effective_type =~ /^multipart\/(?:signed|encrypted)/ ) {
+        # if no module claimed that it supports these types then
+        # we don't dive in and check sub-parts
+        $args{'Skip'}{ $entity } = 1;
+        return ();
+    }
+
+    my @res;
+
     # not protected itself, look inside
     push @res, $self->FindProtectedParts(
         %args, Entity => $_, Scattered => 0,

commit 0eaa43d21ea04b7c5dc1c7c96212c53f0320a88a
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Thu Jan 21 22:52:03 2010 +0300

    pass parents into FindScatteredParts

diff --git a/lib/RT/Crypt.pm b/lib/RT/Crypt.pm
index 9f5eb77..f408b36 100644
--- a/lib/RT/Crypt.pm
+++ b/lib/RT/Crypt.pm
@@ -71,16 +71,23 @@ sub FindProtectedParts {
     ) foreach grep !$args{'Skip'}{$_}, $entity->parts;
 
     if ( $args{'Scattered'} ) {
+        my %parent;
         my $filter; $filter = sub {
-            return grep !$args{'Skip'}{$_},
-                $_[0]->is_multipart ? () : $_[0],
-                map $filter->($_), grep !$args{'Skip'}{$_},
+            $parent{$_[0]} = $_[1];
+            return
+                grep !$args{'Skip'}{$_},
+                $_[0]->is_multipart ? () : $_,
+                map $filter->($_, $_[0]), grep !$args{'Skip'}{$_},
                     $_[0]->parts;
         };
         my @parts = $filter->($entity);
         foreach my $protocol ( @protocols ) {
-            my $class = 'RT::Crypt::'. $protocol;
-            my @list = $class->FindScatteredParts( Parts => \@parts, Skip => $args{'Skip'} );
+            my $class = $self->LoadImplementation( $protocol );
+            my @list = $class->FindScatteredParts(
+                Parts   => \@parts,
+                Parents => \%parent,
+                Skip    => $args{'Skip'}
+            );
             next unless @list;
 
             push @res, @list;

commit 074f780dcd4a3d70f92a2ce02bfeee9fc3416a00
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Thu Jan 21 22:52:38 2010 +0300

    set Protocol when we find scattered part(s)

diff --git a/lib/RT/Crypt.pm b/lib/RT/Crypt.pm
index f408b36..916b2de 100644
--- a/lib/RT/Crypt.pm
+++ b/lib/RT/Crypt.pm
@@ -90,6 +90,7 @@ sub FindProtectedParts {
             );
             next unless @list;
 
+            $_->{'Protocol'} = $protocol foreach @list;
             push @res, @list;
             @parts = grep !$args{'Skip'}{$_}, @parts;
         }

commit 6893498d49454b0bfbd22bd38e4cd41a9fb73af3
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Thu Jan 21 22:54:54 2010 +0300

    handle multipart/{singned,encrypted} without protocol set

diff --git a/lib/RT/Crypt/GnuPG.pm b/lib/RT/Crypt/GnuPG.pm
index 554312e..46143fe 100644
--- a/lib/RT/Crypt/GnuPG.pm
+++ b/lib/RT/Crypt/GnuPG.pm
@@ -896,11 +896,32 @@ sub CheckIfProtected {
 
     my $protocol = $entity->head->mime_attr( 'Content-Type.protocol' );
     unless ( $protocol ) {
-        $RT::Logger->error( "Entity is '$type', but has no protocol defined. Skipped" );
-        return ();
+        # if protocol is not set then we can check second part for PGP message
+        $RT::Logger->error( "Entity is '$type', but has no protocol defined. Checking for PGP part" );
+        my $protected = $self->_CheckIfProtectedInline( $entity->parts(1), 1 );
+        return () unless $protected;
+
+        if ( $protected eq 'signature' ) {
+            $RT::Logger->debug("Found part signed according to RFC3156");
+            return (
+                Type      => 'signed',
+                Format    => 'RFC3156',
+                Top       => $entity,
+                Data      => $entity->parts(0),
+                Signature => $entity->parts(1),
+            );
+        } else {
+            $RT::Logger->debug("Found part encrypted according to RFC3156");
+            return (
+                Type   => 'encrypted',
+                Format => 'RFC3156',
+                Top    => $entity,
+                Data   => $entity->parts(1),
+                Info   => $entity->parts(0),
+            );
+        }
     }
-
-    if ( $type eq 'multipart/encrypted' ) {
+    elsif ( $type eq 'multipart/encrypted' ) {
         unless ( $protocol eq 'application/pgp-encrypted' ) {
             $RT::Logger->info( "Skipping protocol '$protocol', only 'application/pgp-encrypted' is supported" );
             return ();
@@ -1028,6 +1049,7 @@ sub FindScatteredParts {
 sub _CheckIfProtectedInline {
     my $self = shift;
     my $entity = shift;
+    my $check_for_signature = shift || 0;
 
     my $io = $entity->open('r');
     unless ( $io ) {
@@ -1035,8 +1057,12 @@ sub _CheckIfProtectedInline {
         return '';
     }
     while ( defined($_ = $io->getline) ) {
-        next unless /^-----BEGIN PGP (SIGNED )?MESSAGE-----/;
-        return $1? 'signed': 'encrypted';
+        if ( /^-----BEGIN PGP (SIGNED )?MESSAGE-----/ ) {
+            return $1? 'signed': 'encrypted';
+        }
+        elsif ( $check_for_signature && !/^-----BEGIN PGP SIGNATURE-----/ ) {
+            return 'signature';
+        }
     }
     $io->close;
     return '';

commit 2a36da90785f714f1208beba10c82344f38e9985
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Thu Jan 21 22:55:56 2010 +0300

    without parents we can not complete detection of some parts

diff --git a/lib/RT/Crypt/GnuPG.pm b/lib/RT/Crypt/GnuPG.pm
index 46143fe..4c2c88f 100644
--- a/lib/RT/Crypt/GnuPG.pm
+++ b/lib/RT/Crypt/GnuPG.pm
@@ -1003,7 +1003,7 @@ sub FindScatteredParts {
             push @res, {
                 Type      => 'signed',
                 Format    => 'Attachment',
-                Top       => $entity,
+                Top       => $args{'Parents'}{$data_part_in},
                 Data      => $data_part_in,
                 Signature => $sig_part,
             };
@@ -1023,7 +1023,7 @@ sub FindScatteredParts {
         push @res, {
             Type    => 'encrypted',
             Format  => 'Attachment',
-            Top     => $entity,
+            Top     => $args{'Parents'}{$part},
             Data    => $part,
         };
     }

commit 7985edbdc88b4b0fd066510773eb65f598b681c2
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Thu Jan 21 22:57:38 2010 +0300

    adjust tests, now we support additional detection

diff --git a/t/mail/crypt-gnupg.t b/t/mail/crypt-gnupg.t
index b05a903..11663fd 100644
--- a/t/mail/crypt-gnupg.t
+++ b/t/mail/crypt-gnupg.t
@@ -3,7 +3,7 @@
 use strict;
 use warnings;
 
-use RT::Test nodata => 1, tests => 92;
+use RT::Test nodata => 1, tests => 95;
 plan skip_all => 'GnuPG required.'
     unless eval 'use GnuPG::Interface; 1';
 plan skip_all => 'gpg executable is required.'
@@ -18,7 +18,7 @@ my $homedir = RT::Test::get_abs_relocatable_dir(File::Spec->updir(),
 
 mkdir $homedir;
 
-use_ok('RT::Crypt::GnuPG');
+use_ok('RT::Crypt');
 use_ok('MIME::Entity');
 
 RT->Config->Set( 'GnuPG',
@@ -246,7 +246,10 @@ diag 'wrong signed/encrypted parts: no protocol' if $ENV{'TEST_VERBOSE'};
     $entity->head->mime_attr( 'Content-Type.protocol' => undef );
 
     my @parts = RT::Crypt->FindProtectedParts( Entity => $entity );
-    is( scalar @parts, 0, 'no protected parts' );
+    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 'wrong signed/encrypted parts: not enought parts' if $ENV{'TEST_VERBOSE'};

commit e9217aa257fdef5cc44ac46bdd3bc6ee271d217f
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Fri Jan 22 16:03:20 2010 +0300

    add option --enable-smime

diff --git a/configure.ac b/configure.ac
index 37f0a6c..192fca8 100755
--- a/configure.ac
+++ b/configure.ac
@@ -314,6 +314,20 @@ fi
 AC_SUBST(RT_GPG)
 
 
+dnl RT's SMIME support
+AC_CHECK_PROG([RT_SMIME], [openssl], "yes", "no")
+AC_ARG_ENABLE(gpg,
+            AC_HELP_STRING([--enable-smime],
+                           [Turns on Secure MIME (SMIME) support]),
+            RT_SMIME=$enableval)
+if test "$RT_SMIME" = yes; then
+        RT_SMIME="1"
+else
+        RT_SMIME="0"
+fi
+AC_SUBST(RT_SMIME)
+
+
 dnl This section maps the variable names this script 'natively' generates
 dnl to their existing names. They should be removed from here as the .in
 dnl files are changed to use the new names.

commit ac79375cf3bbbc1de6af662e8ebe882a7429a73d
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Fri Jan 22 19:08:41 2010 +0300

    bring new config options required for SMIM and genric interface

diff --git a/etc/RT_Config.pm.in b/etc/RT_Config.pm.in
index e904873..565dd25 100755
--- a/etc/RT_Config.pm.in
+++ b/etc/RT_Config.pm.in
@@ -645,37 +645,83 @@ Set($DashboardSubject, '%s Dashboard: %s');
 
 =back
 
-=head1 GnuPG Configuration
+=head1 Cryptography
+
+Description of cryptography capabilities can be found by running the command
+`perldoc L<RT::Crypt>` (or `perldoc lib/RT/Crypt.pm` from RT install
+directory). At this momement support for GnuPG (PGP) and SMIME security
+protocols are supported.
+
+=over 4
+
+=item C<%Crypt>
+
+Options generic for all cryptography protocols.
+
+Set C<ProcessIncomming> to list of security protocols that should be
+analyzed in incomming emails. Note that C<Auth::Crypt> mail plugin
+should be added anyway to the C<@MailPlugins> option.
+
+Set C<RejectOnMissingPrivateKey> to false if you don't want to reject
+emails encrypted for key RT doesn't have and can not decrypt.
+
+Set C<RejectOnBadData> to false if you don't want to reject letters
+with incorrect data.
+
+=back
+
+=cut
+
+Set( %Crypt,
+    ProcessIncomming          => undef, # ['GnuPG', 'SMIME']
+
+    RejectOnMissingPrivateKey => 1,
+    RejectOnBadData           => 1,
+);
+
+=head2 SMIME Configuration
+
+A full description of the SMIME integration can be found 
+by running the command `perldoc L<RT::Crypt::SMIME>`  (or `perldoc
+lib/RT/Crypt/SMIME.pm` from your RT install directory).
+
+=over 4
+
+=item C<%SMIME>
+
+=back
+
+=cut
+
+Set( %SMIME,
+    Enable => @RT_SMIME@,
+);
+
+=head2 GnuPG Configuration
 
 A full description of the (somewhat extensive) GnuPG integration can be found 
 by running the command `perldoc L<RT::Crypt::GnuPG>`  (or `perldoc
-        lib/RT/Crypt/GnuPG.pm` from your RT install directory).
+lib/RT/Crypt/GnuPG.pm` from your RT install directory).
 
 =over 4
 
 =item C<%GnuPG>
 
+Set C<Enable> to false or true value to disable or enable GnuPG interfaces
+for encryptng and signing outgoing messages.
+
 Set C<OutgoingMessagesFormat> to 'inline' to use inline encryption and
 signatures instead of 'RFC' (GPG/MIME: RFC3156 and RFC1847) format.
 
 If you want to allow people to encrypt attachments inside the DB then
-set C<AllowEncryptDataInDB> to true
-
-Set C<RejectOnMissingPrivateKey> to false if you don't want to reject
-emails encrypted for key RT doesn't have and can not decrypt.
-
-Set C<RejectOnBadData> to false if you don't want to reject letters
-with incorrect GnuPG data.
+set C<AllowEncryptDataInDB> to true.
 
 =cut
 
 Set( %GnuPG,
-    Enable => @RT_GPG@,
+    Enable                 => @RT_GPG@,
     OutgoingMessagesFormat => 'RFC', # Inline
     AllowEncryptDataInDB   => 0,
-
-    RejectOnMissingPrivateKey => 1,
-    RejectOnBadData           => 1,
 );
 
 =item C<%GnuPGOptions>

commit 6a051d5c1824c9c8323e2330c0759d6201c80fa6
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Fri Jan 22 19:14:50 2010 +0300

    check old and new crypt related config options

diff --git a/lib/RT/Config.pm b/lib/RT/Config.pm
index 0c03b57..1dcaa5e 100644
--- a/lib/RT/Config.pm
+++ b/lib/RT/Config.pm
@@ -421,8 +421,22 @@ our %META = (
         },
     },
     Plugins      => { Type => 'ARRAY' },
+    Crypt        => {
+        Type => 'HASH',
+        PostLoadCheck => sub {
+            my $self = shift;
+            my $opt = $self->Get('Crypt');
+            unless ( $opt->{'ProcessIncomming'} && @{ $opt->{'ProcessIncomming'} } ) {
+                require RT::Crypt;
+                my @enabled = grep $self->Get($_)->{'Enabled'}, RT::Crypt->Protocols;
+                $opt->{'ProcessIncomming'} = \@enabled;
+            }
+        },
+    },
+    SMIME        => { Type => 'HASH' },
     GnuPG        => { Type => 'HASH' },
-    GnuPGOptions => { Type => 'HASH',
+    GnuPGOptions => {
+        Type => 'HASH',
         PostLoadCheck => sub {
             my $self = shift;
             my $gpg = $self->Get('GnuPG');
@@ -437,7 +451,6 @@ our %META = (
                 return;
             }
 
-
             require RT::Crypt::GnuPG;
             unless (RT::Crypt::GnuPG->Probe()) {
                 $RT::Logger->debug(
@@ -445,6 +458,13 @@ our %META = (
                     " PGP support has been disabled");
                 $gpg->{'Enable'} = 0;
             }
+
+            if ( grep exists $gpg->{$_}, qw(RejectOnMissingPrivateKey RejectOnBadData) ) {
+                $RT::Logger->error(
+                    "RejectOnMissingPrivateKey and RejectOnBadData GnuPG options"
+                    ." are now generic to GnuPG and SMIME."
+                );
+            }
         }
     },
 );

commit 6dd172cc16b5bd6953101674464913c602fed66c
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Fri Jan 22 19:23:04 2010 +0300

    base class for cryptography protocols

diff --git a/lib/RT/Crypt/Base.pm b/lib/RT/Crypt/Base.pm
new file mode 100644
index 0000000..b9a8ddd
--- /dev/null
+++ b/lib/RT/Crypt/Base.pm
@@ -0,0 +1,18 @@
+use strict;
+use warnings;
+
+package RT::Crypt::Base;
+
+sub SignEncrypt {
+    return (exit_code => 1, status => []);    
+}
+
+sub VerifyDecrypt {
+    return (exit_code => 1, status => []);
+}
+
+sub CheckIfProtected { return () }
+
+sub FindScatteredParts { return () }
+
+1;

commit 40ba3532023cd465ff8e9301a15c7d1abb8e6c82
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Sat Jan 23 20:48:47 2010 +0300

    change cryptography config
    
    * Crypt->{'Incomming'} and Crypt->{'Outgoing'}
    * set Crypt->{'Enable'} in post load check if any protocol is enabled
      don't announce this

diff --git a/etc/RT_Config.pm.in b/etc/RT_Config.pm.in
index 565dd25..dac3c63 100755
--- a/etc/RT_Config.pm.in
+++ b/etc/RT_Config.pm.in
@@ -673,7 +673,8 @@ with incorrect data.
 =cut
 
 Set( %Crypt,
-    ProcessIncomming          => undef, # ['GnuPG', 'SMIME']
+    Incomming                 => undef, # ['GnuPG', 'SMIME']
+    Outgoing                  => 'GnuPG', # 'SMIME'
 
     RejectOnMissingPrivateKey => 1,
     RejectOnBadData           => 1,
diff --git a/lib/RT/Config.pm b/lib/RT/Config.pm
index 1dcaa5e..5405d69 100644
--- a/lib/RT/Config.pm
+++ b/lib/RT/Config.pm
@@ -425,11 +425,16 @@ our %META = (
         Type => 'HASH',
         PostLoadCheck => sub {
             my $self = shift;
+            require RT::Crypt;
+
             my $opt = $self->Get('Crypt');
-            unless ( $opt->{'ProcessIncomming'} && @{ $opt->{'ProcessIncomming'} } ) {
-                require RT::Crypt;
-                my @enabled = grep $self->Get($_)->{'Enabled'}, RT::Crypt->Protocols;
-                $opt->{'ProcessIncomming'} = \@enabled;
+            my @enabled = RT::Crypt->EnabledProtocols;
+            $opt->{'Enable'} = scalar @enabled;;
+            unless ( $opt->{'Incomming'} && @{ $opt->{'Incomming'} } ) {
+                $opt->{'Incomming'} = \@enabled;
+            }
+            unless ( $opt->{'Outgoing'} ) {
+                $opt->{'Outgoing'} = $enabled[0];
             }
         },
     },
@@ -441,6 +446,7 @@ our %META = (
             my $self = shift;
             my $gpg = $self->Get('GnuPG');
             return unless $gpg->{'Enable'};
+
             my $gpgopts = $self->Get('GnuPGOptions');
             unless (-d $gpgopts->{homedir}  && -r _ ) { # no homedir, no gpg
                 $RT::Logger->debug(

commit 15bbb1dba6b14100c55fa989a3d120d73f5b0e15
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Sat Jan 23 20:52:08 2010 +0300

    minor GnuPG changes
    
    * base class
    * turn another function into method

diff --git a/lib/RT/Crypt/GnuPG.pm b/lib/RT/Crypt/GnuPG.pm
index 4c2c88f..e6a829d 100644
--- a/lib/RT/Crypt/GnuPG.pm
+++ b/lib/RT/Crypt/GnuPG.pm
@@ -50,6 +50,7 @@ use strict;
 use warnings;
 
 package RT::Crypt::GnuPG;
+use base 'RT::Crypt::Base';
 
 use IO::Handle;
 use GnuPG::Interface;
@@ -2016,7 +2017,7 @@ sub GetKeysForEncryption {
         next if $key->{'Capabilities'} =~ /D/;
         # skip keys not suitable for encryption
         next unless $key->{'Capabilities'} =~ /e/i;
-        # skip disabled, expired, revoke and keys with no trust,
+        # skip disabled, expired, revoked and keys with no trust,
         # but leave keys with unknown trust level
         next if $key->{'TrustLevel'} < 0;
 
@@ -2165,12 +2166,13 @@ sub GetKeysInfo {
         return %res;
     }
 
-    @info = ParseKeysInfo( @info );
+    @info = $self->ParseKeysInfo( @info );
     $res{'info'} = \@info;
     return %res;
 }
 
 sub ParseKeysInfo {
+    my $self = shift;
     my @lines = @_;
 
     my %gpg_opt = RT->Config->Get('GnuPGOptions');

commit 03c15b6afd69968351c9c0df596652f778e2fe00
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Sat Jan 23 20:55:41 2010 +0300

    config based methods in RT::Crypt
    
    EnabledProtocols, UseForOutgoing and EnabledOnIncomming

diff --git a/lib/RT/Crypt.pm b/lib/RT/Crypt.pm
index 916b2de..a3a4a85 100644
--- a/lib/RT/Crypt.pm
+++ b/lib/RT/Crypt.pm
@@ -13,8 +13,17 @@ sub Protocols {
     return @PROTOCOLS;
 }
 
+sub EnabledProtocols {
+    my $self = shift;
+    return grep RT->Config->Get($_)->{'Enable'}, $self->Protocols;
+}
+
+sub UseForOutgoing {
+    return RT->Config->Get('Crypt')->{'Outgoing'};
+}
+
 sub EnabledOnIncoming {
-    return 'GnuPG', 'SMIME';
+    return @{ scalar RT->Config->Get('Crypt')->{'Incomming'} };
 }
 
 { my %cache;

commit 0c58710080c84bbb1df42164233f9bf212db0b5b
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Sat Jan 23 21:00:57 2010 +0300

    return asap if there is no parts for scattered search

diff --git a/lib/RT/Crypt.pm b/lib/RT/Crypt.pm
index a3a4a85..672dad1 100644
--- a/lib/RT/Crypt.pm
+++ b/lib/RT/Crypt.pm
@@ -90,6 +90,8 @@ sub FindProtectedParts {
                     $_[0]->parts;
         };
         my @parts = $filter->($entity);
+        return @res unless @parts;
+
         foreach my $protocol ( @protocols ) {
             my $class = $self->LoadImplementation( $protocol );
             my @list = $class->FindScatteredParts(

commit 5825d4f1d7061bac51c13accfc956652c6f51825
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Sat Jan 23 21:02:08 2010 +0300

    generic crypt interface for keys information retrieval

diff --git a/lib/RT/Crypt.pm b/lib/RT/Crypt.pm
index 672dad1..e6101a4 100644
--- a/lib/RT/Crypt.pm
+++ b/lib/RT/Crypt.pm
@@ -170,4 +170,29 @@ sub ParseStatus {
     return $self->LoadImplementation( $args{'Protocol'} )->ParseStatus( $args{'Status'} );
 }
 
+
+sub GetPublicKeyInfo {
+    return (shift)->GetKeyInfo( @_, Type => 'public' );
+}
+
+sub GetPrivateKeyInfo {
+    return (shift)->GetKeyInfo( @_, Type => 'private' );
+}
+
+sub GetKeyInfo {
+    my $self = shift;
+    my %res = $self->GetKeysInfo( @_ );
+    $res{'info'} = $res{'info'}->[0];
+    return %res;
+}
+
+sub GetKeysInfo {
+    my $self = shift;
+    my %args = ( Protocol => undef, @_ );
+    my $protocol = delete $args{'Protocol'} || $self->UseForOutgoing;
+    my %res = $self->LoadImplementation( $protocol )->GetKeysInfo( @_ );
+    $res{'Protocol'} = $protocol;
+    return %res;
+}
+
 1;

commit 41b0a917ff168befec20b9d1e7f876dfae7df2ae
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Sat Jan 23 21:05:17 2010 +0300

    clean GnuPG's keys info implementation

diff --git a/lib/RT/Crypt/Base.pm b/lib/RT/Crypt/Base.pm
index b9a8ddd..940d907 100644
--- a/lib/RT/Crypt/Base.pm
+++ b/lib/RT/Crypt/Base.pm
@@ -15,4 +15,11 @@ sub CheckIfProtected { return () }
 
 sub FindScatteredParts { return () }
 
+sub GetKeysInfo {
+    return (
+        exit_code => 1,
+        message => 'Not implemented',
+    );
+}
+
 1;
diff --git a/lib/RT/Crypt/GnuPG.pm b/lib/RT/Crypt/GnuPG.pm
index e6a829d..1e8aa69 100644
--- a/lib/RT/Crypt/GnuPG.pm
+++ b/lib/RT/Crypt/GnuPG.pm
@@ -2008,7 +2008,7 @@ also listed.
 sub GetKeysForEncryption {
     my $self = shift;
     my $key_id = shift;
-    my %res = $self->GetKeysInfo( $key_id, 'public', @_ );
+    my %res = $self->GetKeysInfo( Key => $key_id, Type => 'public', @_ );
     return %res if $res{'exit_code'};
     return %res unless $res{'info'};
 
@@ -2030,7 +2030,7 @@ sub GetKeysForEncryption {
 sub GetKeysForSigning {
     my $self = shift;
     my $key_id = shift;
-    return $self->GetKeysInfo( $key_id, 'private', @_ );
+    return $self->GetKeysInfo( Key => $key_id, Type => 'private', @_ );
 }
 
 sub CheckRecipients {
@@ -2099,29 +2099,19 @@ sub CheckRecipients {
     return ($status, @issues);
 }
 
-sub GetPublicKeyInfo {
-    return (shift)->GetKeyInfo( shift, 'public', @_ );
-}
-
-sub GetPrivateKeyInfo {
-    return (shift)->GetKeyInfo( shift, 'private', @_ );
-}
-
-sub GetKeyInfo {
-    my $self = shift;
-    my %res = $self->GetKeysInfo(@_);
-    $res{'info'} = $res{'info'}->[0];
-    return %res;
-}
-
 sub GetKeysInfo {
     my $self = shift;
-    my $email = shift;
-    my $type = shift || 'public';
-    my $force = shift;
+    my %args = (
+        Key   => undef,
+        Type  => 'public',
+        Force => 0,
+        @_
+    );
 
+    my $email = $args{'Key'};
+    my $type = $args{'Type'};
     unless ( $email ) {
-        return (exit_code => 0) unless $force;
+        return (exit_code => 0) unless $args{'Force'};
     }
 
     my $gnupg = new GnuPG::Interface;

commit 07fc350a87d696bbb6815b32b66118febe1362f6
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Sat Jan 23 21:08:44 2010 +0300

    in Admin interface show keys info for every enabled protocol

diff --git a/share/html/Admin/Elements/ShowKeyInfo b/share/html/Admin/Elements/ShowKeyInfo
index 35d38a7..4f99fb5 100644
--- a/share/html/Admin/Elements/ShowKeyInfo
+++ b/share/html/Admin/Elements/ShowKeyInfo
@@ -45,13 +45,22 @@
 %# those contributions and any derivatives thereof.
 %# 
 %# END BPS TAGGED BLOCK }}}
-<&| /Widgets/TitleBox, title => $title &>
-% if ( $res{'exit_code'} || !keys %{ $res{'info'} } ) {
-<% loc('No keys for this address') %>
+<&| /Widgets/TitleBox, title => $title &><table>
+% while ( my $protocol = shift @protocols ) {
+% my %res = RT::Crypt->GetKeyInfo(
+%     Protocol => $protocol,
+%     Key      => $EmailAddress,
+%     Type     => $Type,
+% );
+% if ( $res{'exit_code'} ) {
+<tr><th colspan="2"><% loc("Couldn't get [_1] keys information", $protocol) %></th></tr>
+% } elsif ( !keys %{ $res{'info'} } ) {
+<tr><th colspan="2"><% loc('No [_1] keys for this address', $protocol) %></th></tr>
 % } else {
-<table>
 
-% unless ( $Type eq 'private' ) {
+<tr><th colspan="2"><% loc("[_1] key '[_2]'", $protocol, $res{'info'}{'id'} || $res{'info'}{'Fingerprint'} ) %></th></tr>
+
+% if ( $Type ne 'private' && $res{'info'}{'Trust'} ) {
 <tr><th><% loc('Trust') %>:</th>  <td><% loc( $res{'info'}{'Trust'} ) %></td></tr>
 % }
 
@@ -69,23 +78,27 @@
 </td></tr>
 % }
 
-</table>
 % }
-</&>
+
+% if ( @protocols ) {
+<tr><th colspan="2">&nbsp;</th></tr>
+% }
+
+% }
+</table></&>
 
 <%ARGS>
 $EmailAddress
 $Type => 'public'
 </%ARGS>
 <%INIT>
-require RT::Crypt::GnuPG;
-my %res = RT::Crypt::GnuPG::GetKeyInfo( $EmailAddress, $Type );
+my @protocols = RT::Crypt->EnabledProtocols;
+return unless @protocols;
 
 my $title;
 unless ( $Type eq 'private' ) {
-    $title = loc('GnuPG public key(s) for [_1]', $EmailAddress);
+    $title = loc('Public key(s) for [_2]', $EmailAddress);
 } else {
-    $title = loc('GnuPG private key(s) for [_1]', $EmailAddress);
+    $title = loc('Private key(s) for [_2]', $EmailAddress);
 }
-
 </%INIT>

commit 7fe58768ead9ec1f35c022cec4d591f123301c74
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Sat Jan 23 21:11:31 2010 +0300

    variouse fixes in web ui regarding crypto changes

diff --git a/share/html/Admin/Queues/Modify.html b/share/html/Admin/Queues/Modify.html
index df97a68..757acdb 100755
--- a/share/html/Admin/Queues/Modify.html
+++ b/share/html/Admin/Queues/Modify.html
@@ -107,7 +107,7 @@
 </td></tr>
 % }
 
-% if ( RT->Config->Get('GnuPG')->{'Enable'} ) {
+% if ( RT->Config->Get('Crypt')->{'Enable'} ) {
 <tr><td align="right"><input type="checkbox" class="checkbox" name="Sign" value="1" <% $QueueObj->Sign? 'checked="checked"': '' |n%> /></td>
 <td><&|/l&>Sign by default</&></td>
 <td align="right"><input type="checkbox" class="checkbox" name="Encrypt" value="1" <% $QueueObj->Encrypt? 'checked="checked"': '' |n%> /></td>
@@ -120,7 +120,7 @@
 % $m->callback( %ARGS, QueueObj => $QueueObj, results => \@results );
 </td></tr>
 
-% if ( RT->Config->Get('GnuPG')->{'Enable'} ) {
+% if ( RT->Config->Get('Crypt')->{'Enable'} ) {
 % if ( my $email = $QueueObj->CorrespondAddress || RT->Config->Get('CorrespondAddress') ) {
 <tr><td colspan="4"><& /Admin/Elements/ShowKeyInfo, Type => 'private', EmailAddress => $email &></td></tr>
 % }
diff --git a/share/html/Elements/GnuPG/SignEncryptWidget b/share/html/Elements/GnuPG/SignEncryptWidget
index a5bdd23..38bca7a 100644
--- a/share/html/Elements/GnuPG/SignEncryptWidget
+++ b/share/html/Elements/GnuPG/SignEncryptWidget
@@ -133,7 +133,7 @@ if ( $self->{'Sign'} ) {
         push @{ $self->{'GnuPGCanNotSignAs'} ||= [] }, $address;
         $checks_failure = 1;
     } else {
-        RT::Crypt::GnuPG::UseKeyForSigning( $self->{'SignUsing'} )
+        RT::Crypt::GnuPG->UseKeyForSigning( $self->{'SignUsing'} )
             if $self->{'SignUsing'};
     }
 }
@@ -168,13 +168,13 @@ if ( $self->{'Encrypt'} ) {
     my %seen;
     @recipients = grep !$seen{ lc $_ }++, @recipients;
 
-    RT::Crypt::GnuPG::UseKeyForEncryption(
+    RT::Crypt::GnuPG->UseKeyForEncryption(
         map { (/^UseKey-(.*)$/)[0] => $self->{ $_ } }
         grep $self->{ $_ } && /^UseKey-/,
         keys %$self
     );
 
-    my ($status, @issues) = RT::Crypt::GnuPG::CheckRecipients( @recipients );
+    my ($status, @issues) = RT::Crypt::GnuPG->CheckRecipients( @recipients );
     push @{ $self->{'GnuPGRecipientsKeyIssues'} ||= [] }, @issues;
     $checks_failure = 1 unless $status;
 }
diff --git a/share/html/Elements/RT__Ticket/ColumnMap b/share/html/Elements/RT__Ticket/ColumnMap
index 7df4710..af01539 100644
--- a/share/html/Elements/RT__Ticket/ColumnMap
+++ b/share/html/Elements/RT__Ticket/ColumnMap
@@ -259,7 +259,7 @@ $COLUMN_MAP = {
             my @requestors = $t->Requestors->MemberEmailAddresses;
             for my $email (@requestors)
             {
-                my %key = RT::Crypt::GnuPG::GetKeyInfo($email);
+                my %key = RT::Crypt->GetKeyInfo( Key => $email );
                 if (!defined $key{'info'}) {
                     $email .= loc(" (no pubkey!)");
                 }
@@ -276,7 +276,7 @@ $COLUMN_MAP = {
         value     => sub {
             my $t = shift;
             my $name = $t->OwnerObj->Name;
-            my %key = RT::Crypt::GnuPG::GetKeyInfo($t->OwnerObj->EmailAddress);
+            my %key = RT::Crypt->GetKeyInfo( Key => $t->OwnerObj->EmailAddress );
             if (!defined $key{'info'}) {
                 $name .= ' '. loc("(no pubkey!)");
             }
diff --git a/share/html/Ticket/Elements/ShowGnuPGStatus b/share/html/Ticket/Elements/ShowGnuPGStatus
index fa78bc8..7a81de0 100644
--- a/share/html/Ticket/Elements/ShowGnuPGStatus
+++ b/share/html/Ticket/Elements/ShowGnuPGStatus
@@ -67,7 +67,7 @@ my $re_protocols = join '|', map "\Q$_\E", @protocols;
 
 foreach ( $Attachment->SplitHeaders ) {
     if ( s/^X-RT-($re_protocols)-Status:\s*//io ) {
-        push @runs, [ RT::Crypt->ParseStatus( Protocol => "$1", Status => $_ ) ];
+        push @runs, [ $1, RT::Crypt->ParseStatus( Protocol => "$1", Status => $_ ) ];
     }
 
     $needs_unsigned_warning = 0 if /^X-RT-Incoming-Signature:/;
@@ -135,6 +135,7 @@ my $reverify_cb = sub {
 
 my @messages;
 foreach my $run ( @runs ) {
+    my $protocol = shift @$run;
     foreach my $line ( @$run ) {
         if ( $line->{'Operation'} eq 'KeyCheck' ) {
             next unless $Reverify;
@@ -142,7 +143,9 @@ foreach my $run ( @runs ) {
             next unless $line->{'KeyType'} eq 'public' && $line->{'Status'} eq 'MISSING';
 
             # but only if we have key
-            my %key = RT::Crypt::GnuPG::GetPublicKeyInfo( $line->{'Key'} );
+            my %key = RT::Crypt->GetPublicKeyInfo(
+                Protocol => $protocol, Key => $line->{'Key'}
+            );
             if ( $key{'info'} ) {
                 my ($status, $msg) = $reverify_cb->($Attachment);
                 unless ($status) {

commit 465d7c5be125cc3d2b6fc925b2dbc55130e6609b
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Sat Jan 23 21:18:57 2010 +0300

    update tests

diff --git a/t/mail/gnupg-incoming.t b/t/mail/gnupg-incoming.t
index ec31333..3285a33 100644
--- a/t/mail/gnupg-incoming.t
+++ b/t/mail/gnupg-incoming.t
@@ -29,7 +29,7 @@ RT->Config->Set( 'GnuPGOptions',
                  homedir => $homedir,
                  'no-permission-warning' => undef);
 
-RT->Config->Set( 'MailPlugins' => 'Auth::MailFrom', 'Auth::GnuPG' );
+RT->Config->Set( 'MailPlugins' => 'Auth::MailFrom', 'Auth::Crypt' );
 
 my ($baseurl, $m) = RT::Test->started_ok;
 
diff --git a/t/web/gnupg-select-keys-on-create.t b/t/web/gnupg-select-keys-on-create.t
index deee6b2..a62959d 100644
--- a/t/web/gnupg-select-keys-on-create.t
+++ b/t/web/gnupg-select-keys-on-create.t
@@ -68,7 +68,7 @@ diag "check that signing doesn't work if there is no key" if $ENV{TEST_VERBOSE};
 {
     RT::Test->import_gnupg_key('rt-recipient at example.com');
     RT::Test->trust_gnupg_key('rt-recipient at example.com');
-    my %res = RT::Crypt::GnuPG::GetKeysInfo('rt-recipient at example.com');
+    my %res = RT::Crypt->GetKeysInfo( Key => 'rt-recipient at example.com');
     is $res{'info'}[0]{'TrustTerse'}, 'ultimate', 'ultimately trusted key';
 }
 
@@ -102,7 +102,7 @@ diag "import first key of rt-test\@example.com" if $ENV{TEST_VERBOSE};
 my $fpr1 = '';
 {
     RT::Test->import_gnupg_key('rt-test at example.com', 'public');
-    my %res = RT::Crypt::GnuPG::GetKeysInfo('rt-test at example.com');
+    my %res = RT::Crypt->GetKeysInfo( Key => 'rt-test at example.com');
     is $res{'info'}[0]{'TrustLevel'}, 0, 'is not trusted key';
     $fpr1 = $res{'info'}[0]{'Fingerprint'};
 }
@@ -149,7 +149,7 @@ diag "import a second key of rt-test\@example.com" if $ENV{TEST_VERBOSE};
 my $fpr2 = '';
 {
     RT::Test->import_gnupg_key('rt-test at example.com.2', 'public');
-    my %res = RT::Crypt::GnuPG::GetKeysInfo('rt-test at example.com');
+    my %res = RT::Crypt->GetKeysInfo( Key => 'rt-test at example.com');
     is $res{'info'}[1]{'TrustLevel'}, 0, 'is not trusted key';
     $fpr2 = $res{'info'}[2]{'Fingerprint'};
 }
@@ -194,7 +194,7 @@ diag "check that things still doesn't work if two keys are not trusted" if $ENV{
 
 {
     RT::Test->lsign_gnupg_key( $fpr1 );
-    my %res = RT::Crypt::GnuPG::GetKeysInfo('rt-test at example.com');
+    my %res = RT::Crypt->GetKeysInfo( Key => 'rt-test at example.com');
     ok $res{'info'}[0]{'TrustLevel'} > 0, 'trusted key';
     is $res{'info'}[1]{'TrustLevel'}, 0, 'is not trusted key';
 }
diff --git a/t/web/gnupg-select-keys-on-update.t b/t/web/gnupg-select-keys-on-update.t
index 76817dd..432faac 100644
--- a/t/web/gnupg-select-keys-on-update.t
+++ b/t/web/gnupg-select-keys-on-update.t
@@ -80,7 +80,7 @@ diag "check that signing doesn't work if there is no key" if $ENV{TEST_VERBOSE};
 {
     RT::Test->import_gnupg_key('rt-recipient at example.com');
     RT::Test->trust_gnupg_key('rt-recipient at example.com');
-    my %res = RT::Crypt::GnuPG::GetKeysInfo('rt-recipient at example.com');
+    my %res = RT::Crypt->GetKeysInfo( Key => 'rt-recipient at example.com');
     is $res{'info'}[0]{'TrustTerse'}, 'ultimate', 'ultimately trusted key';
 }
 
@@ -116,7 +116,7 @@ diag "import first key of rt-test\@example.com" if $ENV{TEST_VERBOSE};
 my $fpr1 = '';
 {
     RT::Test->import_gnupg_key('rt-test at example.com', 'public');
-    my %res = RT::Crypt::GnuPG::GetKeysInfo('rt-test at example.com');
+    my %res = RT::Crypt->GetKeysInfo( Key => 'rt-test at example.com');
     is $res{'info'}[0]{'TrustLevel'}, 0, 'is not trusted key';
     $fpr1 = $res{'info'}[0]{'Fingerprint'};
 }
@@ -164,7 +164,7 @@ diag "import a second key of rt-test\@example.com" if $ENV{TEST_VERBOSE};
 my $fpr2 = '';
 {
     RT::Test->import_gnupg_key('rt-test at example.com.2', 'public');
-    my %res = RT::Crypt::GnuPG::GetKeysInfo('rt-test at example.com');
+    my %res = RT::Crypt->GetKeysInfo( Key => 'rt-test at example.com');
     is $res{'info'}[1]{'TrustLevel'}, 0, 'is not trusted key';
     $fpr2 = $res{'info'}[2]{'Fingerprint'};
 }
@@ -210,7 +210,7 @@ diag "check that things still doesn't work if two keys are not trusted" if $ENV{
 
 {
     RT::Test->lsign_gnupg_key( $fpr1 );
-    my %res = RT::Crypt::GnuPG::GetKeysInfo('rt-test at example.com');
+    my %res = RT::Crypt->GetKeysInfo( Key => 'rt-test at example.com');
     ok $res{'info'}[0]{'TrustLevel'} > 0, 'trusted key';
     is $res{'info'}[1]{'TrustLevel'}, 0, 'is not trusted key';
 }

commit 9b82eabc126fff18312f18a4b8f79e5d70128eaf
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Mon Jan 25 09:57:36 2010 +0300

    refactor UseKeyFor* and GetKeysFor* for generic use

diff --git a/lib/RT/Attachment_Overlay.pm b/lib/RT/Attachment_Overlay.pm
index 6a97966..7d6d405 100644
--- a/lib/RT/Attachment_Overlay.pm
+++ b/lib/RT/Attachment_Overlay.pm
@@ -662,9 +662,9 @@ sub Encrypt {
         RT->Config->Get('CorrespondAddress'),
         RT->Config->Get('CommentAddress'),
     ) {
-        my %res = RT::Crypt::GnuPG->GetKeysInfo( $address, 'private' );
+        my %res = RT::Crypt->GetKeysInfo( Key => $address, Type => 'private' );
         next if $res{'exit_code'} || !$res{'info'};
-        %res = RT::Crypt::GnuPG::GetKeysForEncryption( $address );
+        %res = RT::Crypt->GetKeysForEncryption( $address );
         next if $res{'exit_code'} || !$res{'info'};
         $encrypt_for = $address;
     }
diff --git a/lib/RT/Crypt.pm b/lib/RT/Crypt.pm
index e6101a4..7063eae 100644
--- a/lib/RT/Crypt.pm
+++ b/lib/RT/Crypt.pm
@@ -137,6 +137,13 @@ sub SignEncrypt {
     return %res;
 }
 
+sub DrySign {
+    my $self = shift;
+    my %args = ( Protocol => undef, Signer => undef, @_ );
+    my $protocol = $args{'Protocol'} || $self->UseForOutgoing;
+    return $self->LoadImplementation( $protocol )->DrySign( @_ );
+}
+
 sub VerifyDecrypt {
     my $self = shift;
     my %args = (
@@ -170,6 +177,59 @@ sub ParseStatus {
     return $self->LoadImplementation( $args{'Protocol'} )->ParseStatus( $args{'Status'} );
 }
 
+=head2 UseKeyForSigning
+
+Returns or sets identifier of the key that should be used for signing.
+
+Returns the current value when called without arguments.
+
+Sets new value when called with one argument and unsets if it's undef.
+
+=cut
+
+{ my $key;
+sub UseKeyForSigning {
+    my $self = shift;
+    if ( @_ ) {
+        $key = $_[0];
+    }
+    return $key;
+} }
+
+{ my %key;
+# no args -> clear
+# one arg -> return preferred key
+# many -> set
+sub UseKeyForEncryption {
+    my $self = shift;
+    unless ( @_ ) {
+        %key = ();
+    } elsif ( @_ > 1 ) {
+        %key = (%key, @_);
+        $key{ lc($_) } = delete $key{ $_ } foreach grep lc ne $_, keys %key;
+    } else {
+        return $key{ $_[0] };
+    }
+    return ();
+} }
+
+sub GetKeysForEncryption {
+    my $self = shift;
+    my %args = @_%2? (Recipient => @_) : (Protocol => undef, For => undef, @_ );
+    my $protocol = delete $args{'Protocol'} || $self->UseForOutgoing;
+    my %res = $self->LoadImplementation( $protocol )->GetKeysForEncryption( @_ );
+    $res{'Protocol'} = $protocol;
+    return %res;
+}
+
+sub GetKeysForSigning {
+    my $self = shift;
+    my %args = @_%2? (Signer => @_) : (Protocol => undef, Signer => undef, @_);
+    my $protocol = delete $args{'Protocol'} || $self->UseForOutgoing;
+    my %res = $self->LoadImplementation( $protocol )->GetKeysForSigning( @_ );
+    $res{'Protocol'} = $protocol;
+    return %res;
+}
 
 sub GetPublicKeyInfo {
     return (shift)->GetKeyInfo( @_, Type => 'public' );
@@ -188,7 +248,7 @@ sub GetKeyInfo {
 
 sub GetKeysInfo {
     my $self = shift;
-    my %args = ( Protocol => undef, @_ );
+    my %args = @_%2 ? (Key => @_) : ( Protocol => undef, Key => undef, @_ );
     my $protocol = delete $args{'Protocol'} || $self->UseForOutgoing;
     my %res = $self->LoadImplementation( $protocol )->GetKeysInfo( @_ );
     $res{'Protocol'} = $protocol;
diff --git a/lib/RT/Crypt/GnuPG.pm b/lib/RT/Crypt/GnuPG.pm
index 1e8aa69..a8455c4 100644
--- a/lib/RT/Crypt/GnuPG.pm
+++ b/lib/RT/Crypt/GnuPG.pm
@@ -504,7 +504,7 @@ sub SignEncryptRFC3156 {
     if ( $args{'Encrypt'} ) {
         my %seen;
         $gnupg->options->push_recipients( $_ ) foreach 
-            map UseKeyForEncryption($_) || $_,
+            map RT::Crypt->UseKeyForEncryption($_) || $_,
             grep !$seen{ $_ }++, map $_->address,
             map Email::Address->parse( $entity->head->get( $_ ) ),
             qw(To Cc Bcc);
@@ -627,7 +627,7 @@ sub _SignEncryptTextInline {
 
     if ( $args{'Encrypt'} ) {
         $gnupg->options->push_recipients( $_ ) foreach 
-            map UseKeyForEncryption($_) || $_,
+            map RT::Crypt->UseKeyForEncryption($_) || $_,
             @{ $args{'Recipients'} || [] };
     }
 
@@ -717,7 +717,7 @@ sub _SignEncryptAttachmentInline {
     my $entity = $args{'Entity'};
     if ( $args{'Encrypt'} ) {
         $gnupg->options->push_recipients( $_ ) foreach
-            map UseKeyForEncryption($_) || $_,
+            map RT::Crypt->UseKeyForEncryption($_) || $_,
             @{ $args{'Recipients'} || [] };
     }
 
@@ -819,7 +819,7 @@ sub SignEncryptContent {
 
     if ( $args{'Encrypt'} ) {
         $gnupg->options->push_recipients( $_ ) foreach 
-            map UseKeyForEncryption($_) || $_,
+            map RT::Crypt->UseKeyForEncryption($_) || $_,
             @{ $args{'Recipients'} || [] };
     }
 
@@ -1962,40 +1962,6 @@ sub _PrepareGnuPGOptions {
     return %res;
 }
 
-{ my %key;
-# no args -> clear
-# one arg -> return preferred key
-# many -> set
-sub UseKeyForEncryption {
-    unless ( @_ ) {
-        %key = ();
-    } elsif ( @_ > 1 ) {
-        %key = (%key, @_);
-        $key{ lc($_) } = delete $key{ $_ } foreach grep lc ne $_, keys %key;
-    } else {
-        return $key{ $_[0] };
-    }
-    return ();
-} }
-
-=head2 UseKeyForSigning
-
-Returns or sets identifier of the key that should be used for signing.
-
-Returns the current value when called without arguments.
-
-Sets new value when called with one argument and unsets if it's undef.
-
-=cut
-
-{ my $key;
-sub UseKeyForSigning {
-    if ( @_ ) {
-        $key = $_[0];
-    }
-    return $key;
-} }
-
 =head2 GetKeysForEncryption
 
 Takes identifier and returns keys suitable for encryption.
@@ -2007,8 +1973,8 @@ also listed.
 
 sub GetKeysForEncryption {
     my $self = shift;
-    my $key_id = shift;
-    my %res = $self->GetKeysInfo( Key => $key_id, Type => 'public', @_ );
+    my %args = (Recipient => undef, @_);
+    my %res = $self->GetKeysInfo( Key => delete $args{'Recipient'}, %args, Type => 'public' );
     return %res if $res{'exit_code'};
     return %res unless $res{'info'};
 
@@ -2029,8 +1995,8 @@ sub GetKeysForEncryption {
 
 sub GetKeysForSigning {
     my $self = shift;
-    my $key_id = shift;
-    return $self->GetKeysInfo( Key => $key_id, Type => 'private', @_ );
+    my %args = (Signer => undef, @_)
+    return $self->GetKeysInfo( Key => delete $args{'Signer'}, %args, Type => 'private' );
 }
 
 sub CheckRecipients {
@@ -2041,7 +2007,7 @@ sub CheckRecipients {
 
     my %seen;
     foreach my $address ( grep !$seen{ lc $_ }++, map $_->address, @recipients ) {
-        my %res = $self->GetKeysForEncryption( $address );
+        my %res = $self->GetKeysForEncryption( Recipient => $address );
         if ( $res{'info'} && @{ $res{'info'} } == 1 && $res{'info'}[0]{'TrustLevel'} > 0 ) {
             # good, one suitable and trusted key 
             next;
@@ -2051,7 +2017,7 @@ sub CheckRecipients {
         # it's possible that we have no User record with the email
         $user = undef unless $user->id;
 
-        if ( my $fpr = UseKeyForEncryption( $address ) ) {
+        if ( my $fpr = RT::Crypt->UseKeyForEncryption( $address ) ) {
             if ( $res{'info'} && @{ $res{'info'} } ) {
                 next if
                     grep lc $_->{'Fingerprint'} eq lc $fpr,
@@ -2404,10 +2370,10 @@ sub ImportKey {
     return %res;
 }
 
-=head2 KEY
+=head2 DrySign Signer => KEY
 
 Signs a small message with the key, to make sure the key exists and 
-we have a useable passphrase. The first argument MUST be a key identifier
+we have a useable passphrase. The Signer argument MUST be a key identifier
 of the signer: either email address, key id or finger print.
 
 Returns a true value if all went well.
@@ -2416,7 +2382,8 @@ Returns a true value if all went well.
 
 sub DrySign {
     my $self = shift;
-    my $from = shift;
+    my %args = ( Signer => undef, @_ );
+    my $from = $args{'Signer'};
 
     my $mime = MIME::Entity->build(
         Type    => "text/plain",
@@ -2443,7 +2410,6 @@ sub DrySign {
 This routine returns true if RT's GnuPG support is configured and working 
 properly (and false otherwise).
 
-
 =cut
 
 
diff --git a/lib/RT/Interface/Web/Handler.pm b/lib/RT/Interface/Web/Handler.pm
index 8d1be8d..06d5f59 100644
--- a/lib/RT/Interface/Web/Handler.pm
+++ b/lib/RT/Interface/Web/Handler.pm
@@ -185,7 +185,7 @@ and is not recommended to change.
 
 =item Clean up state of RT::Action::SendEmail using 'CleanSlate' method
 
-=item Flush tmp GnuPG key preferences
+=item Flush tmp crypt key preferences
 
 =back
 
@@ -212,10 +212,10 @@ sub CleanupRequest {
     require RT::Action::SendEmail;
     RT::Action::SendEmail->CleanSlate;
     
-    if (RT->Config->Get('GnuPG')->{'Enable'}) {
-        require RT::Crypt::GnuPG;
-        RT::Crypt::GnuPG::UseKeyForEncryption();
-        RT::Crypt::GnuPG::UseKeyForSigning( undef );
+    if (RT->Config->Get('Crypt')->{'Enable'}) {
+        require RT::Crypt;
+        RT::Crypt->UseKeyForEncryption();
+        RT::Crypt->UseKeyForSigning( undef );
     }
 
     %RT::Ticket::MERGE_CACHE = ( effective => {}, merged => {} );
diff --git a/lib/RT/User_Overlay.pm b/lib/RT/User_Overlay.pm
index db3964c..6e90bcc 100755
--- a/lib/RT/User_Overlay.pm
+++ b/lib/RT/User_Overlay.pm
@@ -1645,19 +1645,19 @@ sub PreferredKey
     return $prefkey->Content if $prefkey;
 
     # we don't have a preferred key for this user, so now we must query GPG
-    require RT::Crypt::GnuPG;
-    my %res = RT::Crypt::GnuPG::GetKeysForEncryption($self->EmailAddress);
+    require RT::Crypt;
+    my %res = RT::Crypt->GetKeysForEncryption($self->EmailAddress);
     return undef unless defined $res{'info'};
     my @keys = @{ $res{'info'} };
     return undef if @keys == 0;
 
     if (@keys == 1) {
-        $prefkey = $keys[0]->{'Fingerprint'};
+        $prefkey = $keys[0]->{'id'} || $keys[0]->{'Fingerprint'};
     }
     else {
         # prefer the maximally trusted key
         @keys = sort { $b->{'TrustLevel'} <=> $a->{'TrustLevel'} } @keys;
-        $prefkey = $keys[0]->{'Fingerprint'};
+        $prefkey = $keys[0]->{'id'} || $keys[0]->{'Fingerprint'};
     }
 
     $self->SetAttribute(Name => 'PreferredKey', Content => $prefkey);
@@ -1686,7 +1686,7 @@ sub SetPrivateKey {
 
     # check that it's really private key
     {
-        my %tmp = RT::Crypt::GnuPG::GetKeysForSigning( $key );
+        my %tmp = RT::Crypt->GetKeysForSigning( $key );
         return (0, $self->loc("No such key or it's not suitable for signing"))
             if $tmp{'exit_code'} || !$tmp{'info'};
     }
diff --git a/share/html/Admin/Users/GnuPG.html b/share/html/Admin/Users/GnuPG.html
index dd0d05a..1f5daf3 100644
--- a/share/html/Admin/Users/GnuPG.html
+++ b/share/html/Admin/Users/GnuPG.html
@@ -82,7 +82,7 @@ $Update     => undef
 <%INIT>
 return unless RT->Config->Get('GnuPG')->{'Enable'};
 
-require RT::Crypt::GnuPG;
+require RT::Crypt;
 
 my @results;
 
@@ -94,7 +94,7 @@ unless ( $UserObj->id ) {
 $id = $ARGS{'id'} = $UserObj->id;
 
 my $email = $UserObj->EmailAddress;
-my %keys_meta = RT::Crypt::GnuPG::GetKeysForSigning( $email, 'force' );
+my %keys_meta = RT::Crypt->GetKeysForSigning( $email, Force => 1 );
 
 $ARGS{'PrivateKey'} = $m->comp('/Widgets/Form/Select:Process',
     Name      => 'PrivateKey',
diff --git a/share/html/Elements/GnuPG/SelectKeyForEncryption b/share/html/Elements/GnuPG/SelectKeyForEncryption
index c28c5ad..c93047d 100644
--- a/share/html/Elements/GnuPG/SelectKeyForEncryption
+++ b/share/html/Elements/GnuPG/SelectKeyForEncryption
@@ -50,7 +50,7 @@
 % } else {
 <select name="<% $Name %>">
 %     foreach my $key (@keys) {
-<option value="<% $key->{'Fingerprint'} %>"><% $key->{'Fingerprint'} %> <% loc("(trust: [_1])", $key->{'TrustTerse'}) %></option>
+<option value="<% $key->{'id'} || $key->{'Fingerprint'} %>"><% $key->{'id'} || $key->{'Fingerprint'} %> <% loc("(trust: [_1])", $key->{'TrustTerse'}) %></option>
 %     }
 </select>
 % }
@@ -59,10 +59,10 @@
 require RT::Crypt::GnuPG;
 my $d;
 
-my %res = RT::Crypt::GnuPG::GetKeysForEncryption($EmailAddress);
+my %res = RT::Crypt->GetKeysForEncryption($EmailAddress);
 # move the preferred key to the top of the list
 my @keys = map {
-               $_->{'Fingerprint'} eq ( $Default || '' )
+               ($_->{'id'} || $_->{'Fingerprint'}) eq ( $Default || '' )
                    ?  do { $d = $_; () }
                    : $_
            }
@@ -78,4 +78,3 @@ $Name         => 'PreferredKey'
 $EmailAddress => undef
 $Default      => undef
 </%ARGS>
-
diff --git a/share/html/Elements/GnuPG/SignEncryptWidget b/share/html/Elements/GnuPG/SignEncryptWidget
index 38bca7a..f3c2b1d 100644
--- a/share/html/Elements/GnuPG/SignEncryptWidget
+++ b/share/html/Elements/GnuPG/SignEncryptWidget
@@ -65,9 +65,7 @@ return unless $self;
 $Arguments => {}
 </%ARGS>
 <%INIT>
-return undef unless RT->Config->Get('GnuPG')->{'Enable'};
-
-require RT::Crypt::GnuPG;
+return undef unless RT->Config->Get('Crypt')->{'Enable'};
 return { %$Arguments };
 </%INIT>
 </%METHOD>
@@ -129,11 +127,11 @@ if ( $self->{'Sign'} ) {
         ? ( $QueueObj->CommentAddress || RT->Config->Get('CommentAddress') )
         : ( $QueueObj->CorrespondAddress || RT->Config->Get('CorrespondAddress') );
 
-    unless ( RT::Crypt::GnuPG->DrySign( $address ) ) {
+    unless ( RT::Crypt->DrySign( Signer => $address ) ) {
         push @{ $self->{'GnuPGCanNotSignAs'} ||= [] }, $address;
         $checks_failure = 1;
     } else {
-        RT::Crypt::GnuPG->UseKeyForSigning( $self->{'SignUsing'} )
+        RT::Crypt->UseKeyForSigning( $self->{'SignUsing'} )
             if $self->{'SignUsing'};
     }
 }
@@ -168,7 +166,7 @@ if ( $self->{'Encrypt'} ) {
     my %seen;
     @recipients = grep !$seen{ lc $_ }++, @recipients;
 
-    RT::Crypt::GnuPG->UseKeyForEncryption(
+    RT::Crypt->UseKeyForEncryption(
         map { (/^UseKey-(.*)$/)[0] => $self->{ $_ } }
         grep $self->{ $_ } && /^UseKey-/,
         keys %$self

commit 4eed6a44791e3e329e678aee87e034c18ffd2274
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Wed Jan 27 13:02:12 2010 +0300

    missing semicolon

diff --git a/lib/RT/Crypt/GnuPG.pm b/lib/RT/Crypt/GnuPG.pm
index a8455c4..1cb9907 100644
--- a/lib/RT/Crypt/GnuPG.pm
+++ b/lib/RT/Crypt/GnuPG.pm
@@ -1995,7 +1995,7 @@ sub GetKeysForEncryption {
 
 sub GetKeysForSigning {
     my $self = shift;
-    my %args = (Signer => undef, @_)
+    my %args = (Signer => undef, @_);
     return $self->GetKeysInfo( Key => delete $args{'Signer'}, %args, Type => 'private' );
 }
 

commit 728fac9d394d9706ac04fc6a46c08f36ab01af26
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Thu Jan 28 21:16:15 2010 +0300

    fix parts_DFS like filter we have, we need only leafs

diff --git a/lib/RT/Crypt.pm b/lib/RT/Crypt.pm
index 7063eae..43631f8 100644
--- a/lib/RT/Crypt.pm
+++ b/lib/RT/Crypt.pm
@@ -83,11 +83,11 @@ sub FindProtectedParts {
         my %parent;
         my $filter; $filter = sub {
             $parent{$_[0]} = $_[1];
-            return
-                grep !$args{'Skip'}{$_},
-                $_[0]->is_multipart ? () : $_,
-                map $filter->($_, $_[0]), grep !$args{'Skip'}{$_},
-                    $_[0]->parts;
+            unless ( $_[0]->is_multipart ) {
+                return () if $args{'Skip'}{$_[0]};
+                return $_[0];
+            }
+            return map $filter->($_, $_[0]), grep !$args{'Skip'}{$_}, $_[0]->parts;
         };
         my @parts = $filter->($entity);
         return @res unless @parts;

commit 84013125d42abf8ec06ace1c41b0e95e41b1c3c3
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Thu Jan 28 21:17:03 2010 +0300

    don't report unsafe permissions on gpg tests

diff --git a/t/mail/gnupg-incoming.t b/t/mail/gnupg-incoming.t
index 3285a33..44f48df 100644
--- a/t/mail/gnupg-incoming.t
+++ b/t/mail/gnupg-incoming.t
@@ -84,6 +84,7 @@ run3(
         '--default-key' => 'recipient at example.com',
         '--homedir'     => $homedir,
         '--passphrase'  => 'recipient',
+        '--no-permission-warning',
     ),
     \"fnord\r\n",
     \$buf,
@@ -126,6 +127,7 @@ run3(
         '--default-key' => 'recipient at example.com',
         '--homedir'     => $homedir,
         '--passphrase'  => 'recipient',
+        '--no-permission-warning',
     ),
     \"clearfnord\r\n",
     \$buf,
@@ -168,6 +170,7 @@ run3(
         '--default-key' => 'recipient at example.com',
         '--homedir'     => $homedir,
         '--passphrase'  => 'recipient',
+        '--no-permission-warning',
     ),
     \"orzzzzzz\r\n",
     \$buf,
@@ -216,6 +219,7 @@ run3(
         '--default-key' => 'rt at example.com',
         '--homedir'     => $homedir,
         '--passphrase'  => 'test',
+        '--no-permission-warning',
     ),
     \"alright\r\n",
     \$buf,
@@ -251,6 +255,7 @@ run3(
         qw(gpg --armor --encrypt),
         '--recipient'   => 'random at localhost',
         '--homedir'     => $homedir,
+        '--no-permission-warning',
     ),
     \"should not be there either\r\n",
     \$buf,
@@ -288,6 +293,7 @@ run3(
         qw(gpg --armor --encrypt),
         '--recipient'   => 'rt at example.com',
         '--homedir'     => $homedir,
+        '--no-permission-warning',
     ),
     \"really should not be there either\r\n",
     \$buf,

commit 8a4f064951f7e27f29666ee8c7745706f5f0480c
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Mon Feb 1 23:44:15 2010 +0300

    s/Incomming/Incoming/

diff --git a/lib/RT/Config.pm b/lib/RT/Config.pm
index 5405d69..ebcec0c 100644
--- a/lib/RT/Config.pm
+++ b/lib/RT/Config.pm
@@ -425,13 +425,14 @@ our %META = (
         Type => 'HASH',
         PostLoadCheck => sub {
             my $self = shift;
+
             require RT::Crypt;
+            my @enabled = RT::Crypt->EnabledProtocols;
 
             my $opt = $self->Get('Crypt');
-            my @enabled = RT::Crypt->EnabledProtocols;
             $opt->{'Enable'} = scalar @enabled;;
-            unless ( $opt->{'Incomming'} && @{ $opt->{'Incomming'} } ) {
-                $opt->{'Incomming'} = \@enabled;
+            unless ( $opt->{'Incoming'} && @{ $opt->{'Incoming'} } ) {
+                $opt->{'Incoming'} = \@enabled;
             }
             unless ( $opt->{'Outgoing'} ) {
                 $opt->{'Outgoing'} = $enabled[0];

commit 7941c4d6497d58398c19791703b5309e92f8fd13
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Mon Feb 1 23:44:40 2010 +0300

    PostLoadCheck for SMIME config option

diff --git a/lib/RT/Config.pm b/lib/RT/Config.pm
index ebcec0c..6e453af 100644
--- a/lib/RT/Config.pm
+++ b/lib/RT/Config.pm
@@ -439,7 +439,33 @@ our %META = (
             }
         },
     },
-    SMIME        => { Type => 'HASH' },
+    SMIME        => {
+        Type => 'HASH',
+        PostLoadCheck => sub {
+            my $self = shift;
+            my $opt = $self->Get('SMIME');
+            return unless $opt->{'Enable'};
+
+            my $path = $opt->{'OpenSSL'};
+            if ( defined $path ) {
+                unless ( -e $path && -x _) {
+                    $RT::Logger->debug(
+                        "'$path' doesn't exist or is not an executable."
+                        ." Please change SMIME->OpenSSL option."
+                        ." SMIME support has been disabled"
+                    );
+                    $opt->{'Enable'} = 0;
+                    return;
+                }
+            } else {
+                $opt->{'OpenSSL'} = 'openssl';
+            }
+
+            unless ( ref $opt->{'Passphrase'} eq 'HASH' ) {
+                $opt->{'Passphrase'} = { '' => $opt->{'Passphrase'} };
+            }
+        },
+    },
     GnuPG        => { Type => 'HASH' },
     GnuPGOptions => {
         Type => 'HASH',

commit aec5bd62834658ac042246df2f0ff3be2fa4addf
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Mon Feb 1 23:46:00 2010 +0300

    new functions for testing: smime, temp storang, relocatable

diff --git a/lib/RT/Test.pm b/lib/RT/Test.pm
index 62e84e9..a2ae2d8 100644
--- a/lib/RT/Test.pm
+++ b/lib/RT/Test.pm
@@ -210,6 +210,50 @@ sub bootstrap_tempdir {
     );
 }
 
+sub new_temp_file {
+    my $self = shift;
+    my @keys = @_;
+    my $name = pop @keys;
+
+    my $path = File::Spec->catfile( "$tmp{'directory'}", $name );
+
+    my $last_key = pop @keys;
+    my $tmp = \%tmp;
+    while ( my $key = shift @keys ) {
+        $tmp = ($tmp->{$key} ||= {});
+    }
+    return $tmp->{$last_key} = $path;
+}
+
+sub new_temp_dir {
+    my $self = shift;
+    my @keys = @_;
+    my $name = pop @keys;
+
+    my $path = File::Spec->catdir( "$tmp{'directory'}", $name );
+    mkpath( $path );
+
+    my $last_key = pop @keys;
+    my $tmp = \%tmp;
+    while ( my $key = shift @keys ) {
+        $tmp = ($tmp->{$key} ||= {});
+    }
+    return $tmp->{$last_key} = $path;
+}
+
+sub temp {
+    my $self = shift;
+    my @keys = @_;
+
+    my $last_key = pop @keys;
+    my $tmp = \%tmp;
+    while ( my $key = shift @keys ) {
+        return undef unless $tmp->{$key};
+        $tmp = $tmp->{$key};
+    }
+    return $tmp->{$last_key};
+}
+
 sub bootstrap_config {
     my $self = shift;
     my %args = @_;
@@ -217,10 +261,11 @@ sub bootstrap_config {
     $tmp{'config'}{'RT'} = File::Spec->catfile(
         "$tmp{'directory'}", 'RT_SiteConfig.pm'
     );
-    open my $config, '>', $tmp{'config'}{'RT'}
-        or die "Couldn't open $tmp{'config'}{'RT'}: $!";
+    my $config = $self->new_temp_file( config => RT => 'RT_SiteConfig.pm' );
+    open my $config_fh, '>', $config
+        or die "Couldn't open $config: $!";
 
-    print $config qq{
+    print $config_fh qq{
 Set( \$WebPort , $port);
 Set( \$WebBaseURL , "http://localhost:\$WebPort");
 Set( \$LogToSyslog , undef);
@@ -228,20 +273,20 @@ Set( \$LogToScreen , "warning");
 Set( \$MailCommand, 'testfile');
 };
     if ( $ENV{'RT_TEST_DB_SID'} ) { # oracle case
-        print $config "Set( \$DatabaseName , '$ENV{'RT_TEST_DB_SID'}' );\n";
-        print $config "Set( \$DatabaseUser , '$dbname');\n";
+        print $config_fh "Set( \$DatabaseName , '$ENV{'RT_TEST_DB_SID'}' );\n";
+        print $config_fh "Set( \$DatabaseUser , '$dbname');\n";
     } else {
-        print $config "Set( \$DatabaseName , '$dbname');\n";
-        print $config "Set( \$DatabaseUser , 'u${dbname}');\n";
+        print $config_fh "Set( \$DatabaseName , '$dbname');\n";
+        print $config_fh "Set( \$DatabaseUser , 'u${dbname}');\n";
     }
-    print $config "Set( \$DevelMode, 0 );\n"
+    print $config_fh "Set( \$DevelMode, 0 );\n"
         if $INC{'Devel/Cover.pm'};
 
     # set mail catcher
     my $mail_catcher = $tmp{'mailbox'} = File::Spec->catfile(
         $tmp{'directory'}->dirname, 'mailbox.eml'
     );
-    print $config <<END;
+    print $config_fh <<END;
 Set( \$MailCommand, sub {
     my \$MIME = shift;
 
@@ -254,11 +299,11 @@ Set( \$MailCommand, sub {
 } );
 END
 
-    print $config $args{'config'} if $args{'config'};
+    print $config_fh $args{'config'} if $args{'config'};
 
-    print $config "\n1;\n";
-    $ENV{'RT_SITE_CONFIG'} = $tmp{'config'}{'RT'};
-    close $config;
+    print $config_fh "\n1;\n";
+    $ENV{'RT_SITE_CONFIG'} = $config;
+    close $config_fh;
 
     return $config;
 }
@@ -279,7 +324,7 @@ sub set_config_wrapper {
                 SCALAR => '$',
             );
             my $sigil = $sigils{$type} || $sigils{'SCALAR'};
-            open my $fh, '>>', $tmp{'config'}{'RT'}
+            open my $fh, '>>', RT::Test->temp( config => 'RT' )
                 or die "Couldn't open config file: $!";
             require Data::Dumper;
             print $fh
@@ -815,6 +860,21 @@ sub get_relocatable_file {
     return File::Spec->catfile(get_relocatable_dir(@_), $file);
 }
 
+sub find_relocatable_path {
+    my @path = @_;
+
+    # simple strategy find data/gnupg/keys, from the dir where test file lives
+    # to updirs, try 3 times in total
+    my $path = File::Spec->catfile( @path );
+    for my $up ( 0 .. 2 ) {
+        my $p = get_relocatable_dir($path);
+        return $p if -e $p;
+
+        $path = File::Spec->catfile( File::Spec->updir(), $path );
+    }
+    return undef;
+}
+
 sub get_abs_relocatable_dir {
     (my $volume, my $directories, my $file) = File::Spec->splitpath($0);
     if (File::Spec->file_name_is_absolute($directories)) {
@@ -829,34 +889,19 @@ sub import_gnupg_key {
     my $key  = shift;
     my $type = shift || 'secret';
 
+    my $path = find_relocatable_path( 'data', 'gnupg', 'keys' );
+
     $key =~ s/\@/-at-/g;
     $key .= ".$type.key";
 
-    require RT::Crypt::GnuPG;
-
-    # simple strategy find data/gnupg/keys, from the dir where test file lives
-    # to updirs, try 3 times in total
-    my $path = File::Spec->catfile( 'data', 'gnupg', 'keys' );
-    my $abs_path;
-    for my $up ( 0 .. 2 ) {
-        my $p = get_relocatable_dir($path);
-        if ( -e $p ) {
-            $abs_path = $p;
-            last;
-        }
-        else {
-            $path = File::Spec->catfile( File::Spec->updir(), $path );
-        }
-    }
-
     die "can't find the dir where gnupg keys are stored"
-      unless $abs_path;
+      unless $path;
 
+    require RT::Crypt::GnuPG;
     return RT::Crypt::GnuPG->ImportKey(
-        RT::Test->file_content( [ $abs_path, $key ] ) );
+        RT::Test->file_content( [ $path, $key ] ) );
 }
 
-
 sub lsign_gnupg_key {
     my $self = shift;
     my $key = shift;
@@ -982,6 +1027,29 @@ sub trust_gnupg_key {
     return %res;
 }
 
+sub import_smime_key {
+    my $self = shift;
+    my $key  = shift;
+
+    my $path = find_relocatable_path( 'data', 'smime', 'keys' );
+    die "can't find the dir where smime keys are stored"
+        unless $path;
+
+    my $keyring = RT->Config->Get('SMIME')->{'Keyring'};
+    die "SMIME keyring '$keyring' doesn't exist"
+        unless $keyring && -e $keyring;
+
+    $key .= ".pem";
+
+    my $content = RT::Test->file_content( [ $path, $key ] );
+    open my $fh, '>:raw', File::Spec->catfile($keyring, $key)
+        or die "can't open file: $!";
+    print $fh $content;
+    close $fh;
+
+    return;
+}
+
 sub started_ok {
     my $self = shift;
 
@@ -1028,12 +1096,6 @@ sub start_apache_server {
 
     my %info = $self->apache_server_info( variant => $variant );
 
-    Test::More::diag(do {
-        open my $fh, '<', $tmp{'config'}{'RT'};
-        local $/;
-        <$fh>
-    });
-
     my $tmpl = File::Spec->rel2abs( File::Spec->catfile(
         't', 'data', 'configs',
         'apache'. $info{'version'} .'+'. $variant .'.conf'
@@ -1056,16 +1118,10 @@ sub start_apache_server {
         my $method = 'apache_'.$variant.'_server_options';
         $self->$method( \%info, \%opt );
     }
-    $tmp{'config'}{'apache'} = File::Spec->catfile(
-        "$tmp{'directory'}", "apache.conf"
-    );
-    $self->process_in_file(
-        in      => $tmpl, 
-        out     => $tmp{'config'}{'apache'},
-        options => \%opt,
-    );
+    my $apache_config = $self->new_temp_file( config => apache => "apache.conf" );
+    $self->process_in_file( in => $tmpl, out => $apache_config, options => \%opt );
 
-    $self->fork_exec($info{'executable'}, '-f', $tmp{'config'}{'apache'});
+    $self->fork_exec($info{'executable'}, '-f', $apache_config);
     my $pid = do {
         my $tries = 10;
         while ( !-e $opt{'pid_file'} ) {

commit 303bab00c3e22f232b0091538fe1328ccf64e494
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Mon Feb 1 23:47:15 2010 +0300

    expect that we get Queue and Actions arguments and pass them further

diff --git a/lib/RT/Interface/Email/Auth/Crypt.pm b/lib/RT/Interface/Email/Auth/Crypt.pm
index e24a32c..b716f82 100644
--- a/lib/RT/Interface/Email/Auth/Crypt.pm
+++ b/lib/RT/Interface/Email/Auth/Crypt.pm
@@ -57,7 +57,7 @@ RT::Interface::Email::Auth::Crypt - decrypting and verifying protected emails
 
 =head2 DESCRIPTION
 
-This mail plugin decrypts and verifies incomming emails. Supported
+This mail plugin decrypts and verifies incoming emails. Supported
 encryption protocols are GnuPG and SMIME.
 
 This code is independant from code that encrypts/sign outgoing emails, so
@@ -92,6 +92,8 @@ sub GetCurrentUser {
     my %args = (
         Message       => undef,
         RawMessageRef => undef,
+        Queue         => undef,
+        Actions       => undef,
         @_
     );
 
@@ -112,7 +114,9 @@ sub GetCurrentUser {
     my $msg = $args{'Message'}->dup;
 
     my ($status, @res) = VerifyDecrypt(
-        Entity => $args{'Message'}, AddStatus => 1,
+        %args,
+        Entity => $args{'Message'},
+        AddStatus => 1,
     );
     if ( $status && !@res ) {
         $args{'Message'}->head->add(

commit d0a2e3301fe15c3bf2c448ef3edbe6fb37899e61
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Mon Feb 1 23:48:01 2010 +0300

    pass "queue" and actions into early mail plugins

diff --git a/lib/RT/Interface/Email.pm b/lib/RT/Interface/Email.pm
index 55ceddb..8a32074 100755
--- a/lib/RT/Interface/Email.pm
+++ b/lib/RT/Interface/Email.pm
@@ -1295,6 +1295,10 @@ sub Gateway {
     push @mail_plugins, "Auth::MailFrom" unless @mail_plugins;
     @mail_plugins = _LoadPlugins( @mail_plugins );
 
+    #Set up a queue object
+    my $SystemQueueObj = RT::Queue->new( $RT::SystemUser );
+    $SystemQueueObj->Load( $args{'queue'} );
+
     my %skip_plugin;
     foreach my $class( grep !ref, @mail_plugins ) {
         # check if we should apply filter before decoding
@@ -1306,6 +1310,8 @@ sub Gateway {
         next unless $check_cb->(
             Message       => $Message,
             RawMessageRef => \$args{'message'},
+            Queue         => $SystemQueueObj,
+            Actions       => \@actions,
         );
 
         $skip_plugin{ $class }++;
@@ -1317,6 +1323,8 @@ sub Gateway {
         my ($status, $msg) = $Code->(
             Message       => $Message,
             RawMessageRef => \$args{'message'},
+            Queue         => $SystemQueueObj,
+            Actions       => \@actions,
         );
         next if $status > 0;
 
@@ -1367,10 +1375,6 @@ sub Gateway {
         $Right = 'CreateTicket';
     }
 
-    #Set up a queue object
-    my $SystemQueueObj = RT::Queue->new( $RT::SystemUser );
-    $SystemQueueObj->Load( $args{'queue'} );
-
     # We can safely have no queue of we have a known-good ticket
     unless ( $SystemTicket->id || $SystemQueueObj->id ) {
         return ( -75, "RT couldn't find the queue: " . $args{'queue'}, undef );

commit 9c33a4fab36f54455fee038df2bf22ed91b00d8f
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Mon Feb 1 23:48:58 2010 +0300

    s/Incomming/Incoming/

diff --git a/lib/RT/Crypt.pm b/lib/RT/Crypt.pm
index 43631f8..610aab1 100644
--- a/lib/RT/Crypt.pm
+++ b/lib/RT/Crypt.pm
@@ -23,7 +23,7 @@ sub UseForOutgoing {
 }
 
 sub EnabledOnIncoming {
-    return @{ scalar RT->Config->Get('Crypt')->{'Incomming'} };
+    return @{ scalar RT->Config->Get('Crypt')->{'Incoming'} };
 }
 
 { my %cache;
@@ -54,7 +54,7 @@ sub FindProtectedParts {
     my @protocols = $args{'Protocols'}
         ? @{ $args{'Protocols'} } 
         : $self->EnabledOnIncoming;
-
+        
     foreach my $protocol ( @protocols ) {
         my $class = $self->LoadImplementation( $protocol );
         my %info = $class->CheckIfProtected( Entity => $entity );

commit 19a00c4a6cb746727d4f0b40b36195602dd1f1e4
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Mon Feb 1 23:49:49 2010 +0300

    add smime data for testing

diff --git a/t/data/smime/keys/MailEncrypted.txt b/t/data/smime/keys/MailEncrypted.txt
new file mode 100644
index 0000000..4500b30
--- /dev/null
+++ b/t/data/smime/keys/MailEncrypted.txt
@@ -0,0 +1,112 @@
+To: recipient at test.com
+From: sender at test.com
+Subject: Some subject
+MIME-Version: 1.0
+Content-Disposition: attachment; filename="smime.p7m"
+Content-Type: application/x-pkcs7-mime; smime-type=enveloped-data; name="smime.p7m"
+Content-Transfer-Encoding: base64
+
+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
+wJRBdsUShcnZ97/lDYsvdmY=
+
diff --git a/t/data/smime/keys/MailForSend.txt b/t/data/smime/keys/MailForSend.txt
new file mode 100644
index 0000000..b28052a
--- /dev/null
+++ b/t/data/smime/keys/MailForSend.txt
@@ -0,0 +1,7 @@
+Some data
+Some data
+Some data
+Some data
+Some data
+Some data
+Some data
diff --git a/t/data/smime/keys/README b/t/data/smime/keys/README
new file mode 100644
index 0000000..57a09f9
--- /dev/null
+++ b/t/data/smime/keys/README
@@ -0,0 +1,60 @@
+To decrypt signed and encrypted messages 
+openssl smime -in MailEncrypted.txt -decrypt  -recip recipient.crt \
+-inkey recipient.key > MailDecrypted.txt
+
+To verify signature
+openssl smime -in MailDecrypted.txt  -verify -out Mail.txt \
+-signer sender.crt -CAfile ca.crt 
+
+Password on all certificates: 123456
+
+###################################################################
+
+If you gonna to run your own CA and/or create your own certificates 
+here is some help. DONT ASK ME ANY SINGLE STUPID QUESTION please. 10x.
+DONT ASK ME ANY BUNCH OF STUPID QUESTIONS EITHER. Thanks.
+
+1. Run CA.pl -newca   it will create directory structure
+Look at misc directory which comes with openssl
+
+2. You may hack defaults in openssl config file, or create your own
+config file. If so run export OPENSSL_CONFIG=`pwd`/openssl.cnf to 
+force openssl to use YOUR config
+
+3. ROOT CA KEY 4096 bits long
+openssl genrsa -des3 -out ca.key 4096 
+
+4. ROOT Self signed certificate for 4 years
+openssl req -new -x509 -days 1460 -key ca.key -out ca.crt
+
+5. Key for user 4096 keys long
+openssl genrsa -des3 -out user.key 4096
+
+6. Certificate signing request for user for 4 years
+openssl req -new -days 1460 -key user.key -out user.csr
+
+7. Sign it with ROOT CA CERT
+openssl ca -in user.csr -out user.crt -cert ca.crt -keyfile ca.key -days 1460
+
+8. To load into Outlook or Mozilla you will need PKCS12 file format.
+NOTE !!!!! NOTE !!!!!
+**** This format joins private key and certificate leaving private key unprotected.
+**** Make sure you provide export password and don't send this file
+**** via open link if you are really paranoid
+****
+openssl pkcs12 -export -in user.crt -out user.p12 -inkey user.key -name "Dear User Personal Certificate" 
+
+
+9. To sign message
+
+openssl smime -sign -in /etc/passwd -signer user.crt -text -inkey user.key > signed.txt
+
+10. To encrypt
+
+openssl smime -encrypt -out encrypted.txt -from user at test.com -to user at test.com -subject "puzzle" -des3 user.crt
+
+11. Hope that helps to prevent III World War and to improve your brain ability.
+So finally it makes our life on this planet more peacefull and enjoyable.
+
+Dmitry Dorofeev.
+dima at yasp.com
\ No newline at end of file
diff --git a/t/data/smime/keys/ca.crt b/t/data/smime/keys/ca.crt
new file mode 100644
index 0000000..9cacaa7
--- /dev/null
+++ b/t/data/smime/keys/ca.crt
@@ -0,0 +1,37 @@
+-----BEGIN CERTIFICATE-----
+MIIGczCCBFugAwIBAgIBADANBgkqhkiG9w0BAQQFADCBhzELMAkGA1UEBhMCUlUx
+EzARBgNVBAgTClNvbWUtU3RhdGUxFjAUBgNVBAcTDVN0LVBldGVyc2J1cmcxDTAL
+BgNVBAoTBFRlc3QxDTALBgNVBAsTBHRlc3QxDzANBgNVBAMTBlRlc3RlcjEcMBoG
+CSqGSIb3DQEJARYNdGVzdEB0ZXN0LmNvbTAeFw0wMzAyMTAxNDA2MDBaFw0wNzAy
+MDkxNDA2MDBaMIGHMQswCQYDVQQGEwJSVTETMBEGA1UECBMKU29tZS1TdGF0ZTEW
+MBQGA1UEBxMNU3QtUGV0ZXJzYnVyZzENMAsGA1UEChMEVGVzdDENMAsGA1UECxME
+dGVzdDEPMA0GA1UEAxMGVGVzdGVyMRwwGgYJKoZIhvcNAQkBFg10ZXN0QHRlc3Qu
+Y29tMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAm6o4DVBUf0CzcwW/
+psHnERPPLuZYXGY+ItoGtdFd0FYFTLFWF7wZJH3kJj3TIRYJ6tF/QXaLFcizqnCV
+VhrdIBOWlyz7oOZ0z+SKkVn6f0rg6NX8RKnWKeVSstn2wy3/EEiRnpnAxH6/4Ra4
+dKWcldvqRzgmrbQL5rWfwN+TOSFUiNC8HL18zPkwoAmq/D8nGvGO6W/5KUFiMRoH
+ZonmcO29rPBf/o1nCABQHeTH0MZoad4c453U8bpYy5iBx5TX7tdMCnVAcWMaU9qE
+N701rlp73n08veX40pVSSPm43caXYco+AZ82sT2MKbhPw/IJauaSflYdUXSKV0PN
+71yCYhQ8/d1L04jiuR+1LnAGeLh1nKmhK8uYA8B6f25/sjZN1rDjriRLa7+xFTTY
+kxnY8T+z/+LylHgEjOi4yfDibjyMoNyJSg/DT3BW04/aZuUAsTrAwAN/JwwW1QN1
+pZu6m0STOINFcdmP+NHpxQ/z1Q8aPwu4eB74vNCitJNrSe90Ow8+ufxaZVsi4+Eq
+HHScKLtdQxPs5rHshIXI+6WZhqoQEBPNkhSRyFkUXTYG07ro1/jeJnKdD/Lz7hYZ
+GSt5/km/NdeYYhRoHVF6z6yhAdX5VUyeCiDQqd7EA9oO3BsYn11G9qJBYSJd7gFQ
+UB8pTZhbINhhZxr2byomHobU9pkCAwEAAaOB5zCB5DAdBgNVHQ4EFgQUkzZg7t4t
+stLSkqP6sGLsN35+LPIwgbQGA1UdIwSBrDCBqYAUkzZg7t4tstLSkqP6sGLsN35+
+LPKhgY2kgYowgYcxCzAJBgNVBAYTAlJVMRMwEQYDVQQIEwpTb21lLVN0YXRlMRYw
+FAYDVQQHEw1TdC1QZXRlcnNidXJnMQ0wCwYDVQQKEwRUZXN0MQ0wCwYDVQQLEwR0
+ZXN0MQ8wDQYDVQQDEwZUZXN0ZXIxHDAaBgkqhkiG9w0BCQEWDXRlc3RAdGVzdC5j
+b22CAQAwDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQQFAAOCAgEAmkPTDqkAixy0
+kNlTdsKdtMQDV+mc2OMNc1s2clYiAqhDx4AznTy6IYg4wEOaxbnjxQ8TnbOLC9cs
+M0lAGh15kPxeEe8yESJgVtj4IP41TPVkR/6+rKD7hP4y5JOSRPhYh9IkTH9byeJl
+6Q6WvQp6psO+eDQZGwR2ceqb6/NbImKfNW73sRm9OHBwz4S2NtmIx4LoqaQjcPck
+z+81gctY16MFs4ILRfa+lydLIyqa97KcMIzntWl2YgkWX9QvlPIF9ornLlAy1rvN
+cRaNdkmA9vOcCmXxmTFttZ5DHCxRYPx4pV8oJMjqW4xLQk9Ob60oUWfBLxwScrTP
+MuzgybzTWrAY5bJakperd35Qat56bgUDvRscgmXKuCeQZ6m1DyeYAxtJfkrOTyTw
+GzUg9+IQX+tcq0UJ77Rx2hUYyINTOuwEEBUF36l0QDO8QWlS9XkF/iif7fQMHqej
+if8umdWWzdEpHWiZMFvTDnjvlF/2htX2PVnRcOXRyLemcPsutum0RJJyCNjgGmJN
+4WcM6kS8lbBgJGJUz3Cs2ILoj6e8XXqgxVF4Cpb3troYuFzPUuWlNOyN8FkAgztU
+cycqvtAUOvAYKwlGbT9oiYyGgraVmmS4Cc/2OZ8BsG5gxk7HfFXRkt8l7U/0DWxq
+++kpxBnI+Tz1RGNNIcfC6M5XB39b+bY=
+-----END CERTIFICATE-----
diff --git a/t/data/smime/keys/ca.key b/t/data/smime/keys/ca.key
new file mode 100644
index 0000000..0f58a24
--- /dev/null
+++ b/t/data/smime/keys/ca.key
@@ -0,0 +1,54 @@
+-----BEGIN RSA PRIVATE KEY-----
+Proc-Type: 4,ENCRYPTED
+DEK-Info: DES-EDE3-CBC,B51FD7E25F477D16
+
+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
+-----END RSA PRIVATE KEY-----
diff --git a/t/data/smime/keys/recipient.crt b/t/data/smime/keys/recipient.crt
new file mode 100644
index 0000000..19768ac
--- /dev/null
+++ b/t/data/smime/keys/recipient.crt
@@ -0,0 +1,124 @@
+Certificate:
+    Data:
+        Version: 3 (0x2)
+        Serial Number: 2 (0x2)
+        Signature Algorithm: sha1WithRSAEncryption
+        Issuer: C=RU, ST=Some-State, L=St-Petersburg, O=Test, OU=test, CN=Tester/emailAddress=test at test.com
+        Validity
+            Not Before: May 23 07:41:48 2007 GMT
+            Not After : Jan 29 07:41:48 2021 GMT
+        Subject: C=RU, ST=Some-State, O=Test, CN=root/emailAddress=root at example.com
+        Subject Public Key Info:
+            Public Key Algorithm: rsaEncryption
+            RSA Public Key: (4096 bit)
+                Modulus (4096 bit):
+                    00:af:b6:6f:04:9f:81:cc:44:a4:b1:f1:0c:c1:7c:
+                    d7:41:9d:8c:10:df:dc:f2:8f:b5:13:ce:f6:03:8d:
+                    0b:b3:f0:bf:b8:dd:cc:f6:4d:26:8c:41:9a:cd:08:
+                    60:36:68:0d:4d:5c:c3:b5:2a:71:cc:39:de:19:19:
+                    61:fb:80:64:5c:ac:64:b8:66:c2:2d:77:2b:8e:5c:
+                    88:82:41:51:f8:d8:8b:35:4f:80:66:57:8c:2e:a4:
+                    cb:4b:4f:c3:38:69:ec:43:c7:00:00:b0:0c:db:01:
+                    50:d2:2c:6e:b0:a5:83:8a:dc:aa:0a:84:a4:4c:82:
+                    f3:77:e1:a9:a2:cc:20:36:80:65:86:62:2b:50:4a:
+                    2c:86:d0:c5:f0:52:7b:ca:5a:04:a9:ed:28:57:bc:
+                    07:e5:e3:a1:a7:60:01:69:09:eb:e5:2c:75:61:53:
+                    4a:e7:00:3b:b6:e9:3c:e2:d2:b3:99:76:d9:5f:70:
+                    a4:7f:3c:d1:46:1e:8e:ea:9d:a3:c0:95:d5:a5:45:
+                    ba:68:64:fb:92:5c:23:05:2e:36:50:aa:0a:68:ba:
+                    23:cd:92:29:42:f8:d6:d8:14:09:e7:f2:52:c5:f1:
+                    9a:c8:9a:aa:4e:bc:26:9f:19:00:bf:46:37:d4:3d:
+                    f3:b8:72:aa:b5:05:a5:94:31:2c:dc:9c:bb:e3:a1:
+                    1e:82:03:ca:29:b5:d4:64:43:51:4c:f2:77:04:bb:
+                    43:97:a3:a5:e7:56:13:47:4c:02:36:db:d9:53:20:
+                    da:bb:a6:d9:54:36:dc:76:15:e0:eb:d7:50:9c:72:
+                    66:6c:7d:59:2f:dd:51:d6:7c:b6:2d:f3:0e:f9:7a:
+                    cf:0f:63:59:3a:6b:8e:3f:c7:76:b0:11:1b:12:e5:
+                    b9:90:66:30:d2:f6:ba:04:de:33:27:58:61:8f:ed:
+                    ed:14:b8:df:2e:5a:b5:cc:ff:a3:72:1c:ef:46:09:
+                    37:1c:81:ca:13:e9:43:9f:38:fb:69:bb:64:21:9e:
+                    e4:b4:44:be:79:61:52:46:cf:a5:83:7f:b5:c7:91:
+                    5b:28:92:17:d2:5e:45:4b:ad:9f:c7:53:fb:1c:3c:
+                    e7:56:92:a7:9f:e7:e3:59:19:36:64:8c:8c:78:00:
+                    2a:f7:93:2d:79:90:91:e1:73:31:24:ca:4b:4d:9b:
+                    1e:35:61:8a:3f:0c:f8:c8:c0:76:6f:01:d8:2b:c1:
+                    e0:0a:f1:fb:c7:a0:2b:48:30:d4:30:bf:47:2b:e5:
+                    b9:8d:64:e1:d4:3a:80:71:60:1e:02:02:d2:1a:f2:
+                    2c:b2:88:d6:21:da:27:83:36:6c:51:45:e3:72:39:
+                    83:16:65:5c:58:e6:14:87:02:95:88:07:f9:b3:20:
+                    58:d9:91
+                Exponent: 65537 (0x10001)
+        X509v3 extensions:
+            X509v3 Basic Constraints: 
+                CA:FALSE
+            Netscape Comment: 
+                OpenSSL Generated Certificate
+            X509v3 Subject Key Identifier: 
+                48:07:DC:D8:10:D8:20:53:F5:7E:23:52:84:81:E7:2C:F3:16:BC:DB
+            X509v3 Authority Key Identifier: 
+                keyid:93:36:60:EE:DE:2D:B2:D2:D2:92:A3:FA:B0:62:EC:37:7E:7E:2C:F2
+
+    Signature Algorithm: sha1WithRSAEncryption
+        74:de:e4:e3:4c:54:07:25:ed:d5:a1:e2:7e:8b:63:1d:a7:2a:
+        e9:73:84:08:dc:00:33:99:12:60:73:c1:a7:a8:01:fa:48:35:
+        87:81:97:7a:86:fc:6d:67:fc:9e:ce:79:52:7f:7e:86:bf:ca:
+        08:cc:2d:58:ac:9b:49:d0:97:bb:75:82:79:93:52:e8:f1:26:
+        eb:1a:90:c3:1c:c3:23:6b:d2:18:a4:d4:11:2c:ac:7a:60:ce:
+        e8:77:b1:73:1a:0a:31:41:02:76:38:78:ef:95:cb:0e:60:35:
+        9e:6d:1f:dc:3a:fc:77:e2:dc:67:34:65:f2:c5:78:c2:96:f7:
+        d7:1a:24:66:61:5c:fb:b9:14:13:7e:5d:81:9e:7f:44:99:33:
+        db:9b:e1:89:89:d8:98:88:02:04:83:63:8b:c2:c5:cf:83:f0:
+        00:c3:81:47:e4:6d:c5:11:cb:13:6d:de:ca:37:f1:68:51:45:
+        ac:c2:8a:69:43:32:de:65:f1:10:09:e1:1c:60:6f:3b:49:39:
+        47:a8:bd:34:f2:a5:12:ec:dd:21:32:8f:41:9c:04:26:33:1a:
+        b3:1b:35:94:2b:69:fe:6c:b6:c2:6e:d0:76:89:09:51:27:e3:
+        f9:35:78:ee:e8:7a:28:3b:f6:60:42:55:82:bb:b0:9d:91:5c:
+        38:0b:9f:2e:02:c6:54:3c:55:0f:09:3c:37:81:52:48:12:1e:
+        42:6c:d7:e1:dd:e1:cc:13:60:fa:7a:e0:02:2d:7d:d9:4e:bb:
+        04:29:f8:8f:c3:73:17:f5:44:76:1d:cd:dc:12:39:0a:2b:a3:
+        fe:19:9d:4f:ed:c6:96:11:96:5a:b2:7e:88:f0:a4:5a:05:63:
+        d6:26:17:d2:98:22:59:f9:32:33:6e:a6:d8:ed:a6:b0:03:c4:
+        d7:18:1c:a9:67:b9:4c:d0:cc:23:f5:d3:36:f2:f4:93:4b:f2:
+        aa:06:91:27:c2:d8:a1:79:ea:88:58:e0:d3:6e:41:2a:2e:58:
+        48:7c:d1:a9:11:a0:c9:93:6b:9b:d1:60:e9:de:eb:a5:92:1c:
+        e3:e3:4b:96:01:6a:e8:21:01:c8:c7:cd:0b:fe:81:6b:8e:33:
+        4e:f0:05:17:c7:a0:de:9d:e6:78:88:7e:3c:1c:6d:51:04:49:
+        a4:4a:62:75:50:dc:55:a4:65:00:2d:c2:55:0b:16:39:24:e2:
+        f7:62:e0:75:d2:45:a2:60:94:36:84:ce:ed:70:9f:f0:eb:96:
+        b8:27:49:4f:a6:e7:52:49:7d:d1:8e:11:12:80:38:0c:92:ca:
+        3f:a2:2a:7b:87:a5:97:7e:a2:ba:b6:d7:ce:82:14:6d:42:52:
+        d5:17:b9:55:d2:a2:f6:0a
+-----BEGIN CERTIFICATE-----
+MIIF3zCCA8egAwIBAgIBAjANBgkqhkiG9w0BAQUFADCBhzELMAkGA1UEBhMCUlUx
+EzARBgNVBAgTClNvbWUtU3RhdGUxFjAUBgNVBAcTDVN0LVBldGVyc2J1cmcxDTAL
+BgNVBAoTBFRlc3QxDTALBgNVBAsTBHRlc3QxDzANBgNVBAMTBlRlc3RlcjEcMBoG
+CSqGSIb3DQEJARYNdGVzdEB0ZXN0LmNvbTAeFw0wNzA1MjMwNzQxNDhaFw0yMTAx
+MjkwNzQxNDhaMGExCzAJBgNVBAYTAlJVMRMwEQYDVQQIEwpTb21lLVN0YXRlMQ0w
+CwYDVQQKEwRUZXN0MQ0wCwYDVQQDEwRyb290MR8wHQYJKoZIhvcNAQkBFhByb290
+QGV4YW1wbGUuY29tMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAr7Zv
+BJ+BzESksfEMwXzXQZ2MEN/c8o+1E872A40Ls/C/uN3M9k0mjEGazQhgNmgNTVzD
+tSpxzDneGRlh+4BkXKxkuGbCLXcrjlyIgkFR+NiLNU+AZleMLqTLS0/DOGnsQ8cA
+ALAM2wFQ0ixusKWDityqCoSkTILzd+GposwgNoBlhmIrUEoshtDF8FJ7yloEqe0o
+V7wH5eOhp2ABaQnr5Sx1YVNK5wA7tuk84tKzmXbZX3CkfzzRRh6O6p2jwJXVpUW6
+aGT7klwjBS42UKoKaLojzZIpQvjW2BQJ5/JSxfGayJqqTrwmnxkAv0Y31D3zuHKq
+tQWllDEs3Jy746EeggPKKbXUZENRTPJ3BLtDl6Ol51YTR0wCNtvZUyDau6bZVDbc
+dhXg69dQnHJmbH1ZL91R1ny2LfMO+XrPD2NZOmuOP8d2sBEbEuW5kGYw0va6BN4z
+J1hhj+3tFLjfLlq1zP+jchzvRgk3HIHKE+lDnzj7abtkIZ7ktES+eWFSRs+lg3+1
+x5FbKJIX0l5FS62fx1P7HDznVpKnn+fjWRk2ZIyMeAAq95MteZCR4XMxJMpLTZse
+NWGKPwz4yMB2bwHYK8HgCvH7x6ArSDDUML9HK+W5jWTh1DqAcWAeAgLSGvIssojW
+IdongzZsUUXjcjmDFmVcWOYUhwKViAf5syBY2ZECAwEAAaN7MHkwCQYDVR0TBAIw
+ADAsBglghkgBhvhCAQ0EHxYdT3BlblNTTCBHZW5lcmF0ZWQgQ2VydGlmaWNhdGUw
+HQYDVR0OBBYEFEgH3NgQ2CBT9X4jUoSB5yzzFrzbMB8GA1UdIwQYMBaAFJM2YO7e
+LbLS0pKj+rBi7Dd+fizyMA0GCSqGSIb3DQEBBQUAA4ICAQB03uTjTFQHJe3VoeJ+
+i2Mdpyrpc4QI3AAzmRJgc8GnqAH6SDWHgZd6hvxtZ/yeznlSf36Gv8oIzC1YrJtJ
+0Je7dYJ5k1Lo8SbrGpDDHMMja9IYpNQRLKx6YM7od7FzGgoxQQJ2OHjvlcsOYDWe
+bR/cOvx34txnNGXyxXjClvfXGiRmYVz7uRQTfl2Bnn9EmTPbm+GJidiYiAIEg2OL
+wsXPg/AAw4FH5G3FEcsTbd7KN/FoUUWswoppQzLeZfEQCeEcYG87STlHqL008qUS
+7N0hMo9BnAQmMxqzGzWUK2n+bLbCbtB2iQlRJ+P5NXju6HooO/ZgQlWCu7CdkVw4
+C58uAsZUPFUPCTw3gVJIEh5CbNfh3eHME2D6euACLX3ZTrsEKfiPw3MX9UR2Hc3c
+EjkKK6P+GZ1P7caWEZZasn6I8KRaBWPWJhfSmCJZ+TIzbqbY7aawA8TXGBypZ7lM
+0Mwj9dM28vSTS/KqBpEnwtiheeqIWODTbkEqLlhIfNGpEaDJk2ub0WDp3uulkhzj
+40uWAWroIQHIx80L/oFrjjNO8AUXx6DeneZ4iH48HG1RBEmkSmJ1UNxVpGUALcJV
+CxY5JOL3YuB10kWiYJQ2hM7tcJ/w65a4J0lPpudSSX3RjhESgDgMkso/oip7h6WX
+fqK6ttfOghRtQlLVF7lV0qL2Cg==
+-----END CERTIFICATE-----
diff --git a/t/data/smime/keys/recipient.csr b/t/data/smime/keys/recipient.csr
new file mode 100644
index 0000000..aeec080
--- /dev/null
+++ b/t/data/smime/keys/recipient.csr
@@ -0,0 +1,28 @@
+-----BEGIN CERTIFICATE REQUEST-----
+MIIEvTCCAqUCAQAwYTELMAkGA1UEBhMCUlUxEzARBgNVBAgTClNvbWUtU3RhdGUx
+DTALBgNVBAoTBFRlc3QxDTALBgNVBAMTBHJvb3QxHzAdBgkqhkiG9w0BCQEWEHJv
+b3RAZXhhbXBsZS5jb20wggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCv
+tm8En4HMRKSx8QzBfNdBnYwQ39zyj7UTzvYDjQuz8L+43cz2TSaMQZrNCGA2aA1N
+XMO1KnHMOd4ZGWH7gGRcrGS4ZsItdyuOXIiCQVH42Is1T4BmV4wupMtLT8M4aexD
+xwAAsAzbAVDSLG6wpYOK3KoKhKRMgvN34amizCA2gGWGYitQSiyG0MXwUnvKWgSp
+7ShXvAfl46GnYAFpCevlLHVhU0rnADu26Tzi0rOZdtlfcKR/PNFGHo7qnaPAldWl
+RbpoZPuSXCMFLjZQqgpouiPNkilC+NbYFAnn8lLF8ZrImqpOvCafGQC/RjfUPfO4
+cqq1BaWUMSzcnLvjoR6CA8optdRkQ1FM8ncEu0OXo6XnVhNHTAI229lTINq7ptlU
+Ntx2FeDr11CccmZsfVkv3VHWfLYt8w75es8PY1k6a44/x3awERsS5bmQZjDS9roE
+3jMnWGGP7e0UuN8uWrXM/6NyHO9GCTccgcoT6UOfOPtpu2QhnuS0RL55YVJGz6WD
+f7XHkVsokhfSXkVLrZ/HU/scPOdWkqef5+NZGTZkjIx4ACr3ky15kJHhczEkyktN
+mx41YYo/DPjIwHZvAdgrweAK8fvHoCtIMNQwv0cr5bmNZOHUOoBxYB4CAtIa8iyy
+iNYh2ieDNmxRReNyOYMWZVxY5hSHApWIB/mzIFjZkQIDAQABoBcwFQYJKoZIhvcN
+AQkHMQgTBjEyMzQ1NjANBgkqhkiG9w0BAQUFAAOCAgEAbD64fjpgPl4mN18HfZQP
+7EyK7cJew7iI0dLmP4rQmVLqWjl+xlvvZdYTbO+Y2OZMU7ihWb2Axcxd/oR7CxZe
+UL43yCWX5eXI0c07/7LGWSNUMdIE7GvOVkRt6o91TLBTLtr/pBoRj368S7SgpajW
+7cykGZroK84+zd2oNjt2aj6j82M+X5limFpEmqzD+wMlSjyNHVVky1RjWQs8wqvr
+Dqmr638aodc7N0KCI/5dfpIYDiHvhUaI8JjvUivFmS6KkYXF7q5uO7poSqs6LL0A
+3FBEnKprFVWkUs19PLfkEoRC7XPoqP5hZfBFPe4eKJvI0hoVlpNtgCf8O9mwMSM0
+AQNtbzZnNWx3ol4eFw3c0MYUCcIMCShtma0oCGbQkgd6NRkg81iMfqw+OevDde1J
+WarhQUsnejt1EUdjpUse/PwoyPSW02Vh4fu+1xrCL8oafPmLy2mxLRJ/qolNvNnG
+Fg3/CM8c7CRnbqStvOD1LdRxIiu0cuPvmsYFQ861EN7cACuQpjL+7nVAffAqamvp
+I7f5F4Qa4//6nfkbxJZZPd2qZyLruhFSdV4kSN8azUOu3ZW+OE/j7TbEdIpzfXZL
+XneNneGvM1jhgcSiqWGG7U6XmU+XT8WbBTtLM+TNOWdFNR/ex6wXsEgatDqesin3
+mJ4yWERo/b9OHNKXP70P/ks=
+-----END CERTIFICATE REQUEST-----
diff --git a/t/data/smime/keys/recipient.key b/t/data/smime/keys/recipient.key
new file mode 100644
index 0000000..1c6e0af
--- /dev/null
+++ b/t/data/smime/keys/recipient.key
@@ -0,0 +1,54 @@
+-----BEGIN RSA PRIVATE KEY-----
+Proc-Type: 4,ENCRYPTED
+DEK-Info: DES-EDE3-CBC,5260BC3BD106CEDF
+
+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
+-----END RSA PRIVATE KEY-----
diff --git a/t/data/smime/keys/sender.csr b/t/data/smime/keys/sender.csr
new file mode 100644
index 0000000..fe7c9ed
--- /dev/null
+++ b/t/data/smime/keys/sender.csr
@@ -0,0 +1,28 @@
+-----BEGIN CERTIFICATE REQUEST-----
+MIIE1jCCAr4CAQAwgZAxCzAJBgNVBAYTAlJVMRMwEQYDVQQIEwpTb21lLVN0YXRl
+MRYwFAYDVQQHEw1TdC1QZXRlcnNidXJnMQ0wCwYDVQQKEwRUZXN0MRQwEgYDVQQL
+EwtTZW5kZXIgdW5pdDEPMA0GA1UEAxMGU2ViZGVyMR4wHAYJKoZIhvcNAQkBFg9z
+ZW5kZXJAdGVzdC5jb20wggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCX
+bJOzfVLNRcCJGqjHFDFKY4O9SftHAgTeU2Drtgp8ZDVWr3euNPc1vppZaljmQjXp
+j04FY67NXTsRVEKncNlJGnxDSrAlKOyv05RcG+w2CG0whdi7s+ods9UM8ir4o5fR
+3mOvlfHD89z3pOS5hejAmSOA1RjJ7iP1XIQ0HSZ/9CWxXJXiI6uYutmBKAqbAAOz
+WgDPtP8tBFpg2r7eZRJoNyf+GBuXaV+v/7Me6LFzQnJ6tyTFTm/mMSxWBHN0aun/
+Q0yQ6OYbSSmg63Bqm0STk9YtciZXjbbR+qGilu/KGrWZm1MwMHacjHDiWXMLVH5O
+ctJ7lIW//4d21h6gBLawFOFlKLS51j8RpQKOR4uLpHZlkoWCe5j1HY/x2ARwZANZ
+BroJVS0TiuRk3XAsbzNcJJdJGeGdPBuAtYqQ24Vdpa6X0jaq0A9Bgzq3k0ng9liD
+ot0a6Y9/bbCfjaAi5uytzWFi7SzIX7ZHK5EU1x7L6wRZdTQ1mAMX38KuaQ9Ok6eO
+AuMgTtiM9vbpw05KO0WQomBnabBXIrajc1l3ibwHExLM9aU0sAxTLnrlcJRFKqFj
+qNWpRaqHZ/Ru+TS/rV+zpzEt/cTwNV1sFEIX1IfpWYZTtq8ASk7Hlw2hqqOA3CfO
+uWyTZUULFA+HrM+ji5twOxBl9jRcnP1GKRs0Q/gc5wIDAQABoAAwDQYJKoZIhvcN
+AQEEBQADggIBABIeM0Jl08qSpBIEL/+MeiRqMJlexQ9awDBn5uB0KzFsISLX8rrP
+G7u1lCEhaFqPmdRDaeyYmN6naiFYNsbIazPHUXHVrbH+Bjax89wBvVw3Q2USdQ/D
+v9NHXVXNgdOxgz9AID7WYvy/xZLfEmbakfAMSLWbDyz6C+hCLVoPWpksO3CNKzsy
+0CS6hZVQS04EXKJeFQQEspr0hiKR0keb0sC3YcuNuYwyUFs/klFazdPIMk6esEkL
+udsgRPAoe2uX7yEx/abC54I62lsg1wBQ9PkWD9dpoW82jzbtvB/dVLqUWyYg57xq
+AU1IClcg8cQbmQaUbsjrAOrwuthR5xWH0Ybn1AwMxUn2z86b2FXYHZlruKlwFSlC
+v4Sz+Icgi9wQDKP4nZTvoc17XgUNjGIUN8xRovCdQ0pul5n6KKrNriRJcbNE9/NN
+b6dH5nUscF0xu4PEqVVt5QuzQioBVNS0jqedIhfyFwmnfdCMUEukMfrTt1DUnf8C
+89sYz66cfa9iNhM82Qgg8K9EdMyebOnxJOES6LZNGecB9n+co/WSATF/f9ih0ShN
+yjMLV05DEYBaitAxmfmZ/C92DZYagcwCUh6FMY9BfBWR0Ts780dl+iF1sn/uJzUd
+OAofD5wGxOIoC44McpnjDefdPMZIN5KFdogEFlKAhozDMC8hzpefeyy+
+-----END CERTIFICATE REQUEST-----
diff --git a/t/data/smime/keys/sender at example.com.crt b/t/data/smime/keys/sender at example.com.crt
new file mode 100644
index 0000000..0c00e95
--- /dev/null
+++ b/t/data/smime/keys/sender at example.com.crt
@@ -0,0 +1,38 @@
+-----BEGIN CERTIFICATE-----
+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
+fVryqg==
+-----END CERTIFICATE-----
diff --git a/t/data/smime/keys/sender at example.com.key b/t/data/smime/keys/sender at example.com.key
new file mode 100644
index 0000000..a0a94ec
--- /dev/null
+++ b/t/data/smime/keys/sender at example.com.key
@@ -0,0 +1,54 @@
+-----BEGIN RSA PRIVATE KEY-----
+Proc-Type: 4,ENCRYPTED
+DEK-Info: DES-EDE3-CBC,D9CF9FD45D1E5DB6
+
+zVl1wndooucgnGLNMqlyKel4sHt682eFMAu6nUipUgOAgDherDUF8lrQk0clhWd1
+UiE7nvXb5QURmJoQ3VVxVYVNQbUj78JBiofLHhGIZxF7vt1raKj676zFP4QPNqQj
+mFu6BLYfKFXjf6HR9QN9TxuTse748jPh0bWuoW3hWS0S4hihaXqQXviw5qNeHnY8
+UxjQ2jC72mIOgN7lz/5E/86XZTa02nIOK/Y7wCikWAxPGwQYcx6eeKxFM8JS3Kde
+ACyctnsRq/GQkFn3q/E+GmCQSQLHUdH5dd6sZ90f25PvERpsP39QPg6hsat1MC44
+CBhl//7fY6uBVjsy2PDkhHp16M+ENBQJENHhkxj49peTKuWzqIzA+yKCELXQ/j5f
+hBnseSFkca0RppPrnV79PTtHuRdeQzeJOWK2tlkIPEaUqlEW5UaQ6VViuqDgop7E
+1cSrNhsnUwLTNlHFymLIcCodL7rmbR+h4YLZ54wWAo16aOZnFDiHCB6EufAWGR9C
+P9tpC+1e2a1FQkVGCPAptyPymF6Cbvzf4rJIPq/oDat9OZQZTvpTI+owIhImQIXH
+SgfXkbYrhOaIURLudvt1lmRHh5mfmNGC6djJ1DUEenlhQNO0RWYKcqMMVa308Bab
+bZVrhPl0l+f00LiWcbR6H2VHEpPIOnycZBxmrMzXMe9buI9QrzlsjV9RRYSA83rW
+frXI5S61ryimXxUYIMoCk0XCO+Y69W6cZ8f9yJpYocQzXaIF3uYBLb3h7NNFGk0t
+qGR26+0M2+Oqm7asfJ4hRRHIAaUtPP78qCPxo2xY7RtAQYidgIhX73dQQjN4vYfa
+BRvV9S8qAyQoNdSIqhPOI4tMFeP/eKlSOjEGkQnoScY2vhCZkdEWlyoZej1gpf+Y
+q2PhXQDSrVzmqskEUNWmhN5LUDVTnT+phxwyQfOthdWRIYg88lg0hKmgS6IV7fU3
+/fcDpPiklITVmtQ6FQnCQzFnnaIcexL3UPr+KVQkwxsGTShf+vgi/VnPzVVVK39x
+hgWeO+vb0YPuABylKsEXdURlyXehOIM+sFLNzz8s5XAYpE0QthmxHtUGHnDp+Zwx
+1jiwH+pXGV/7mZfRiON69o3fZxkrVYojd8rYHcQl2oRdT5vT9cm52VX/plksWLnX
+TW/d25nrx7fP/cW9FDdJoWrrouGC6B2/Gllfgtd0Tf3uXMV4M7ocVIv16U8W8UDW
+3Cb7cKfHHpbDFR2MJWkDhMz83KYs9oLKBvU9AXdCuOMnzqdpiMDiHOWeX3qf5v1S
+Fz3gxvmClp6igdpAjdMCVdb7vI5DqPa+ir5Sd5/dskkTU06BnzvDoysnYlTcp25C
+a9zgif0+jSPW6npGY7wvII28Tulgq/hJqlulU7IiI5CxIYUp2na7l4whwmKZMf50
+hj8RLuY/lru5mDNSdJyBPaAm2/4/DTor7M3IGDGQi6uusoxPVQ5KEJ9RQ2h5MuqU
+2gg1hdwsk1+Y6I9wbBLLrKktSiXsvekbC4HGzAUfSBS3o5Fy/SdXfOrkdglSpXzZ
+lZF7W+wyyZE0tT6Tc52AGq6vZPhkcXDScFdniT200TEYSADp+SDFV1B8sxdLIEyK
+lh6bhEuT859oeT9lzTr/Djr84Y3HMjdXchEU6RY1bHLovOTdZVE3PbfVzgf93VWy
+LqraYR/1zpxmCLCWTrOdjSn0MMHnTQSViKmfJyLpa5p2r6098ImZDDhowzbpU3nn
+UnARcvQIcS6Ci5GyZ9PwYbnaQWeM/lJO3vU53Guk2OjWeVuP8TQOi2eRPIqh3TOw
+df9EkPh4g7rddbEipesIIBDcS2QB5Em7WZ4i5XJ9/4XqdMmcT4ptkHOQVonzLFRQ
+ssZps5Hot0doydbclaJ8h6b6T7YgiUOivjH/cT4vjkmwOD4JT8ZXOlNLe6SoIMfX
+gAZD69K4ZnfLezZ15fO6l0WSFcbAG84G+fJiBcmwC1cc7fo8M7LJTxd2Inzqge1d
+JPeNPFPTayEY99uF+1P6li2EEexleJS0q+KHMoLCldL5DHOotzEoCejQCni1/YZc
+mBJb7DS+qcVU/Mzn/JKpPuZEZIJJGCu/oV3wu4DvCktfL+6JIwiBNWTTPyBY+M8e
+kEKK2fAYtLuAGVtGbbSh35BJiGBgxFUc7RC1fuGHFGoRzun4LcGossMRzYrU1jP5
+GQkbkJvBN4shCgdYCckLwSL+dZK4k3gmYqvzwZvIp+E1+5BgbwlBruAKMMB+A5id
+T1aOG/llQxbpeVk9sNuUZm10FZhTv6buaqM7uAQf4vSekLoTLlUmUkzzJRErJQiQ
+97IKe5vwj3XumyDTq5nGwXklIpRdX3BNN6O8edPQf5xxpeOhh/BZkB1pJdVcKbf7
+qGf2xoStC4KgZ7sJn7sn2ypJjrgNAk9JUtpxJOYF9Kylq1h3Z1EIC4yNGadcn8l7
+SEUsYO7Bw/Vaqq8JsQWfAEFwCmz37aaZpQmpfB2/OyKp+IbmU3og3aek2S21QfFy
+u7gWk6LEvfzVhh8OPmGlgCNJOopkxv5bwlFTQNFGI8YY0kQrWgdNdtlQcPFSl6kt
+fu12/s0T5k56SKvM5QdVfUa3c8dDPq6H4aHlBZnSK74KBS1tvdxWsh6lY6sZdt7c
+c/sBXxaS5AgtTuwjc8HOSOQhv/+hvUnRbDZROBzk6Hp/QWpX13yD9r91eCYysQ80
+10uc7dsH9mRpkaGQA3QPZlD6Ld/2p8h/Hsf/rKQsyOzswdZokmExLnRgVUhVoJ/d
+SoLVfBghHWAD/hvUKhBBPEwp8m9OKm8BF8jF5HsbTL12sBiUeOLpyrJTkRHuiZQn
+Pte4hEimyi835bCop4YbzCDVpbuh1RCgHDpkSSGwqpbiaVJbQCNwGPu9WLezDGl8
+IBwdJDmeg8obAhD+PVETY/wZOaAaE1zrDyoB6Stp5u6bWmO8r5xhrEkjJg2waNpb
+3R/vN6O7G49ZZn+f2D8AI50Fu6LWDuBXKA5IhAH0FQhex48f5Idklvazug+Wxaqp
+zJA1jo+DsmjWuUzcB1PLQr+2zi7PyuhjWKcJuPIz3JD2SYdi/wkyJWNotmiB9heL
+gBMMRpteVNXi0MfCwE5RELU6LUPG4MTon8F+k/I7Y78v4vTiX6/B3ouK9RHCa1d+
+-----END RSA PRIVATE KEY-----
diff --git a/t/data/smime/keys/sender at example.com.pem b/t/data/smime/keys/sender at example.com.pem
new file mode 100644
index 0000000..67e6f54
--- /dev/null
+++ b/t/data/smime/keys/sender at example.com.pem
@@ -0,0 +1,92 @@
+-----BEGIN CERTIFICATE-----
+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
+fVryqg==
+-----END CERTIFICATE-----
+-----BEGIN RSA PRIVATE KEY-----
+Proc-Type: 4,ENCRYPTED
+DEK-Info: DES-EDE3-CBC,D9CF9FD45D1E5DB6
+
+zVl1wndooucgnGLNMqlyKel4sHt682eFMAu6nUipUgOAgDherDUF8lrQk0clhWd1
+UiE7nvXb5QURmJoQ3VVxVYVNQbUj78JBiofLHhGIZxF7vt1raKj676zFP4QPNqQj
+mFu6BLYfKFXjf6HR9QN9TxuTse748jPh0bWuoW3hWS0S4hihaXqQXviw5qNeHnY8
+UxjQ2jC72mIOgN7lz/5E/86XZTa02nIOK/Y7wCikWAxPGwQYcx6eeKxFM8JS3Kde
+ACyctnsRq/GQkFn3q/E+GmCQSQLHUdH5dd6sZ90f25PvERpsP39QPg6hsat1MC44
+CBhl//7fY6uBVjsy2PDkhHp16M+ENBQJENHhkxj49peTKuWzqIzA+yKCELXQ/j5f
+hBnseSFkca0RppPrnV79PTtHuRdeQzeJOWK2tlkIPEaUqlEW5UaQ6VViuqDgop7E
+1cSrNhsnUwLTNlHFymLIcCodL7rmbR+h4YLZ54wWAo16aOZnFDiHCB6EufAWGR9C
+P9tpC+1e2a1FQkVGCPAptyPymF6Cbvzf4rJIPq/oDat9OZQZTvpTI+owIhImQIXH
+SgfXkbYrhOaIURLudvt1lmRHh5mfmNGC6djJ1DUEenlhQNO0RWYKcqMMVa308Bab
+bZVrhPl0l+f00LiWcbR6H2VHEpPIOnycZBxmrMzXMe9buI9QrzlsjV9RRYSA83rW
+frXI5S61ryimXxUYIMoCk0XCO+Y69W6cZ8f9yJpYocQzXaIF3uYBLb3h7NNFGk0t
+qGR26+0M2+Oqm7asfJ4hRRHIAaUtPP78qCPxo2xY7RtAQYidgIhX73dQQjN4vYfa
+BRvV9S8qAyQoNdSIqhPOI4tMFeP/eKlSOjEGkQnoScY2vhCZkdEWlyoZej1gpf+Y
+q2PhXQDSrVzmqskEUNWmhN5LUDVTnT+phxwyQfOthdWRIYg88lg0hKmgS6IV7fU3
+/fcDpPiklITVmtQ6FQnCQzFnnaIcexL3UPr+KVQkwxsGTShf+vgi/VnPzVVVK39x
+hgWeO+vb0YPuABylKsEXdURlyXehOIM+sFLNzz8s5XAYpE0QthmxHtUGHnDp+Zwx
+1jiwH+pXGV/7mZfRiON69o3fZxkrVYojd8rYHcQl2oRdT5vT9cm52VX/plksWLnX
+TW/d25nrx7fP/cW9FDdJoWrrouGC6B2/Gllfgtd0Tf3uXMV4M7ocVIv16U8W8UDW
+3Cb7cKfHHpbDFR2MJWkDhMz83KYs9oLKBvU9AXdCuOMnzqdpiMDiHOWeX3qf5v1S
+Fz3gxvmClp6igdpAjdMCVdb7vI5DqPa+ir5Sd5/dskkTU06BnzvDoysnYlTcp25C
+a9zgif0+jSPW6npGY7wvII28Tulgq/hJqlulU7IiI5CxIYUp2na7l4whwmKZMf50
+hj8RLuY/lru5mDNSdJyBPaAm2/4/DTor7M3IGDGQi6uusoxPVQ5KEJ9RQ2h5MuqU
+2gg1hdwsk1+Y6I9wbBLLrKktSiXsvekbC4HGzAUfSBS3o5Fy/SdXfOrkdglSpXzZ
+lZF7W+wyyZE0tT6Tc52AGq6vZPhkcXDScFdniT200TEYSADp+SDFV1B8sxdLIEyK
+lh6bhEuT859oeT9lzTr/Djr84Y3HMjdXchEU6RY1bHLovOTdZVE3PbfVzgf93VWy
+LqraYR/1zpxmCLCWTrOdjSn0MMHnTQSViKmfJyLpa5p2r6098ImZDDhowzbpU3nn
+UnARcvQIcS6Ci5GyZ9PwYbnaQWeM/lJO3vU53Guk2OjWeVuP8TQOi2eRPIqh3TOw
+df9EkPh4g7rddbEipesIIBDcS2QB5Em7WZ4i5XJ9/4XqdMmcT4ptkHOQVonzLFRQ
+ssZps5Hot0doydbclaJ8h6b6T7YgiUOivjH/cT4vjkmwOD4JT8ZXOlNLe6SoIMfX
+gAZD69K4ZnfLezZ15fO6l0WSFcbAG84G+fJiBcmwC1cc7fo8M7LJTxd2Inzqge1d
+JPeNPFPTayEY99uF+1P6li2EEexleJS0q+KHMoLCldL5DHOotzEoCejQCni1/YZc
+mBJb7DS+qcVU/Mzn/JKpPuZEZIJJGCu/oV3wu4DvCktfL+6JIwiBNWTTPyBY+M8e
+kEKK2fAYtLuAGVtGbbSh35BJiGBgxFUc7RC1fuGHFGoRzun4LcGossMRzYrU1jP5
+GQkbkJvBN4shCgdYCckLwSL+dZK4k3gmYqvzwZvIp+E1+5BgbwlBruAKMMB+A5id
+T1aOG/llQxbpeVk9sNuUZm10FZhTv6buaqM7uAQf4vSekLoTLlUmUkzzJRErJQiQ
+97IKe5vwj3XumyDTq5nGwXklIpRdX3BNN6O8edPQf5xxpeOhh/BZkB1pJdVcKbf7
+qGf2xoStC4KgZ7sJn7sn2ypJjrgNAk9JUtpxJOYF9Kylq1h3Z1EIC4yNGadcn8l7
+SEUsYO7Bw/Vaqq8JsQWfAEFwCmz37aaZpQmpfB2/OyKp+IbmU3og3aek2S21QfFy
+u7gWk6LEvfzVhh8OPmGlgCNJOopkxv5bwlFTQNFGI8YY0kQrWgdNdtlQcPFSl6kt
+fu12/s0T5k56SKvM5QdVfUa3c8dDPq6H4aHlBZnSK74KBS1tvdxWsh6lY6sZdt7c
+c/sBXxaS5AgtTuwjc8HOSOQhv/+hvUnRbDZROBzk6Hp/QWpX13yD9r91eCYysQ80
+10uc7dsH9mRpkaGQA3QPZlD6Ld/2p8h/Hsf/rKQsyOzswdZokmExLnRgVUhVoJ/d
+SoLVfBghHWAD/hvUKhBBPEwp8m9OKm8BF8jF5HsbTL12sBiUeOLpyrJTkRHuiZQn
+Pte4hEimyi835bCop4YbzCDVpbuh1RCgHDpkSSGwqpbiaVJbQCNwGPu9WLezDGl8
+IBwdJDmeg8obAhD+PVETY/wZOaAaE1zrDyoB6Stp5u6bWmO8r5xhrEkjJg2waNpb
+3R/vN6O7G49ZZn+f2D8AI50Fu6LWDuBXKA5IhAH0FQhex48f5Idklvazug+Wxaqp
+zJA1jo+DsmjWuUzcB1PLQr+2zi7PyuhjWKcJuPIz3JD2SYdi/wkyJWNotmiB9heL
+gBMMRpteVNXi0MfCwE5RELU6LUPG4MTon8F+k/I7Y78v4vTiX6/B3ouK9RHCa1d+
+-----END RSA PRIVATE KEY-----
diff --git a/t/data/smime/mails/README b/t/data/smime/mails/README
new file mode 100644
index 0000000..6bb0ee9
--- /dev/null
+++ b/t/data/smime/mails/README
@@ -0,0 +1,16 @@
+Files with MIME entities generated and encryted with testkeys/sender.crt
+in real MUA(look into files for User-Agent)
+
+* simple-txt-enc.eml
+    simple plain/text message with 'test' content
+
+* with-text-attachment.eml
+    multipart message with a text part and a text attachment.
+    Content of the text is 'test'. Name of the attachment
+    is 'attachment.txt' and content is 'text attachment'.
+
+* with-bin-attachment.eml
+    multipart message with a text part and a binary attachment.
+    Content of the text is 'test'. Name of the attachment
+    is 'attachment.bin' and content is 32 random bytes.
+
diff --git a/t/data/smime/mails/simple-txt-enc.eml b/t/data/smime/mails/simple-txt-enc.eml
new file mode 100644
index 0000000..df38c4a
--- /dev/null
+++ b/t/data/smime/mails/simple-txt-enc.eml
@@ -0,0 +1,36 @@
+Date: Fri, 22 Dec 2006 05:01:04 +0300
+From: root at localhost
+X-Mozilla-Draft-Info: internal/draft; vcard=0; receipt=0; uuencode=0
+User-Agent: Thunderbird 1.5.0.9 (X11/20061221)
+MIME-Version: 1.0
+To: sender at test.com
+Subject: test
+Content-Type: application/x-pkcs7-mime; name="smime.p7m"
+Content-Transfer-Encoding: base64
+Content-Disposition: attachment; filename="smime.p7m"
+Content-Description: S/MIME Encrypted Message
+
+MIAGCSqGSIb3DQEHA6CAMIACAQAxggQ8MIIBjgIBADB2MGIxCzAJBgNVBAYTAlpBMSUwIwYD
+VQQKExxUaGF3dGUgQ29uc3VsdGluZyAoUHR5KSBMdGQuMSwwKgYDVQQDEyNUaGF3dGUgUGVy
+c29uYWwgRnJlZW1haWwgSXNzdWluZyBDQQIQBgzQJigV55Om1dc1u4CuYzANBgkqhkiG9w0B
+AQEFAASCAQBEI9JKBQgIqLcRTiYCwDHR1dVTnlOoAAVHoM10dHxzszYBNV5GBnQgPVzV5EbU
+kkjKPJy1Ipv6Eixoqed2u54/68fmvGSEC+zH8nu7noMbvF7nuspPfwawf9GQNq3jt3qZRuuk
+Us2EB5GGz8p9gxgFnv/GtrUQ+7HxCVvJRwFuyXkqwfqo2kxnE4C1jS05xjZ3ioo5gQlncC5f
+ib63YtU6Gvlnh9zq/LV0bMUg6SygRNAHoO+BTKBBHlzNMg0ixUESzRmxF1hJA3rbhCx4xPfe
+OHZh6NtSHPSD+88nzK3qBv1Gosz9In6O5/aYreQgLT6Vrbb+jlCPs3BJGaRPNwVKMIICpgIB
+ADCBjTCBhzELMAkGA1UEBhMCUlUxEzARBgNVBAgTClNvbWUtU3RhdGUxFjAUBgNVBAcTDVN0
+LVBldGVyc2J1cmcxDTALBgNVBAoTBFRlc3QxDTALBgNVBAsTBHRlc3QxDzANBgNVBAMTBlRl
+c3RlcjEcMBoGCSqGSIb3DQEJARYNdGVzdEB0ZXN0LmNvbQIBAzANBgkqhkiG9w0BAQEFAASC
+AgAqLoqQoTHSNgSyp+8XuylkokpE8/zE8mQ0TVnVrP8LrK8ppxBAkVr8/GGG2BNtwKXJEaEP
+7cDoHYABQS4xQD3CrEBtq180rDR0yGunRYuCTlBrAAZWy8Nq3/KGbwxJJBaqHUomaqiRUeiD
+j+V42pU1hVhBVUR+dNlDrtnLyh1OhbR1/ddhU0WCioAAdbyVCntgyHQU0Sr55xVINP69RUte
+OCHQj+s5b6HnfN2pQjdjZf5pHJdCajO5IpKn68LaBY2Q0YZVZYs3PLnRe0yHpZvHa7T5KRNs
+XL4fkJ5n6wrP48UH2eHyopHCww28qZupbnSkhZCERa7mJ3niExaRxnZnS28IpaU4AlbMLZGu
+fz7woFxcxrcwkGeGXCTxZkj6UkXXFZK/s04iWoQdcN59A56yGc0RTHg29AHQshJ2d3Ydcm+N
+/Uv9OwSkDOG/V6f6BvmWjBldF6SI2sOmEEK+SQy0OC81TYJfqVHRfT0lyUfz43JGGwOhgb1j
+hyxBgugzGq2+KkIdyONMdBZXe4HuZOm4MStB/5NOdb1lPttB5zgkcdq9I/rVl+QRSh3wjZWf
+2JL9HvlljweVNbVoCiwQjm17u14PnNlW0797YTXizHlsLUpupMgI8N2eDKv53cKTGyT/z+yc
+AmqCYdph3oA7VWlXBvQbZUeDxOarpfTnq+aOEjCABgkqhkiG9w0BBwEwFAYIKoZIhvcNAwcE
+CBUAZSknCBIhoIAEYHg/qMyTJn4qoHtG5PnCve9BA5m+02B8RFLe1EQ/4+S3r2tP8jSvCPvk
+jMrzK7wrza2xlXaOisFvakPMyjTqwCkup55n3LDELbXMe3eFt62L7mWSD1HI+bwCEM2d7v/5
+DAQIbHw1VJzs8tsAAAAAAAAAAAAA
diff --git a/t/data/smime/mails/with-bin-attachment.eml b/t/data/smime/mails/with-bin-attachment.eml
new file mode 100644
index 0000000..4f4f89d
--- /dev/null
+++ b/t/data/smime/mails/with-bin-attachment.eml
@@ -0,0 +1,45 @@
+Date: Fri, 22 Dec 2006 06:41:22 +0300
+From: root at localhost
+X-Mozilla-Draft-Info: internal/draft; vcard=0; receipt=0; uuencode=0
+User-Agent: Thunderbird 1.5.0.9 (X11/20061221)
+MIME-Version: 1.0
+To:  sender at test.com
+Subject: test
+Content-Type: application/x-pkcs7-mime; name="smime.p7m"
+Content-Transfer-Encoding: base64
+Content-Disposition: attachment; filename="smime.p7m"
+Content-Description: S/MIME Encrypted Message
+
+MIAGCSqGSIb3DQEHA6CAMIACAQAxggQ8MIIBjgIBADB2MGIxCzAJBgNVBAYTAlpBMSUwIwYD
+VQQKExxUaGF3dGUgQ29uc3VsdGluZyAoUHR5KSBMdGQuMSwwKgYDVQQDEyNUaGF3dGUgUGVy
+c29uYWwgRnJlZW1haWwgSXNzdWluZyBDQQIQBgzQJigV55Om1dc1u4CuYzANBgkqhkiG9w0B
+AQEFAASCAQC/2Z7Gd1vC5nSuTH7B1A6HevkiMfA4svuCd+93geSmRfFIKEGIxnjSi6cyD/FO
+DVB7q/+lVA3uDmZ5j2dw15ccxGGYLfq3WjVOPtR3oL3a70LeGHzkeKYBTalENkphR7f4669j
+C8r+3AK6vIGw06h5cCvMFZGsGQZmulga1JS8LcVim1vcmMH4s3CuEIYE3XppU3Dgl4JURI0R
++5inyxpurkWEQ8ACFLBr2N/HK+AANqY8e231YwkiGdGVjhOxYGzWW5V+c5O93C4266wLvg8c
+2SCYMGryh38Zt/TkeNvlTEAYZemgqyaRbkjRY6+y6AAHitDL1LvJj1ADhxJkri9KMIICpgIB
+ADCBjTCBhzELMAkGA1UEBhMCUlUxEzARBgNVBAgTClNvbWUtU3RhdGUxFjAUBgNVBAcTDVN0
+LVBldGVyc2J1cmcxDTALBgNVBAoTBFRlc3QxDTALBgNVBAsTBHRlc3QxDzANBgNVBAMTBlRl
+c3RlcjEcMBoGCSqGSIb3DQEJARYNdGVzdEB0ZXN0LmNvbQIBAzANBgkqhkiG9w0BAQEFAASC
+AgAMJzwFWoFS70JsXSe83zwlkduPHfK+CNeHG0cTizN7TBS/NOgnTK85hGtc5JZSowEpZpkU
+e1O4dYGRl2YHXAAY/J//BrDhj8mLhukIjyfd4/Wy6KDIkP1fvbLPpDNSg98FUtcWSozC3IJy
+soTJPyWSN8Ui5GYX/st/zW2RGPk8fmrX95joodvOJt38AQGpnVFMIpFCqzS1y+JCRR0l1dmb
+5gVn75rFEVTGVNmSguXJaKDGqgwx39QHWhXxpzz166F1L2Ys4s5eKeYjK+9jtqCeTYAjop0f
+E3+W9SHksM+0B9p2l7jUM74/LQNyXcA+l1ab8h6iEIWTRIQ8L5CzJUJGzsSREgffylBAAGEq
+0bV8vQBXIi0YTRhKU1kBfAFBZxlsS2Vmrxn+RGQz11hRjHH69VTyPFV2h978YBnIqt8DoByv
+mLVg+P8r7LvcrCsrKUFAGaHENILbdiKilPUBhV4djmD0Y4pHsEneMinjLa3ayn0mLGWW9KcV
+NaxXdrMg1prLPNY8JRYXSg8zVpPYDW3hG0abvFXKztp+J+dGKXlb3D+VuOoP4FYc2JcGxxdU
+hSTRq8Ee4OtkRGuSTLgXARUYofH4nqAor6+1ixr84QnqK5h61qLPXSJnA6Dox2fUmeRsWm7x
+psCK7Y9v7fFK2WST4LE5fmpyHVtGyOmih4Ug8TCABgkqhkiG9w0BBwEwFAYIKoZIhvcNAwcE
+CGA0oBrKobsjoIAEggIwV0YAtLgEB/F5GEZ8ghp6+H1WJmwn5U9tNNNmzWAif5FytdXoSRL3
+SwJMi0B6IINu0uDV3X47glhdCazJTKiduwF55oEARPPpvFpSqEKPg5KTf47bX0Q41669H9jl
+znFFzgzIZQFl4gpMeg87QJOq+rG3TxWBTOiynBisTT816UrlqYC9LqMEr3laq/psuI0vZLyr
+rr68FrlvO8c21c77RA+oUQ/fFb97SrvmnPpX0DtKLD9Z/n+smPzRA2kUFs+PlbKy7FnH4zX/
+8UCMCYwvtGWXMQA+28aiI4RYw7nJbt+B6FHXQ+ZR9tJ2sVCvSMhGX4ao8UVBLZKA2IE9M2BK
+fX0+o41IhWf5qRT04yVvHlaygCxIKaUzTPu2PTTtez53DPX91s3joLUHi1/a9bpHODuP57dv
+76c1vSg3qJURtVbrAptDpR54DV4bvdRcig6TKoeLw8tjqf0F8glhMIeg6NF7BbUwYtKPL7bm
+0r3bN72/BENBcGyNl/Ou9dZLV3O4+zs1MEoE972LW61AH0voSZVV8Roj0mceSMgpTwU0RY7G
+fzARr/pGh1NwLGVBBYT/5UHIUTuMAVHcZvaFsZjX9kPKnVtTeQjhnYRCfdHVYVoIQnkzn6Sy
+1aGmsv/z6vsF4eSAs9HrF8kwFWRFUJ2YHSl0dqNyvlqvX0VDeK/Ks6ei8AVYvfMdkY5bCbPE
+6KpdkYGyNLJff13Ef1xOcqJgqNWdzGA7S9pnSw+J85UxMKQECNY4jO2xzB+FAAAAAAAAAAAA
+AA==
diff --git a/t/data/smime/mails/with-text-attachment.eml b/t/data/smime/mails/with-text-attachment.eml
new file mode 100644
index 0000000..e18c759
--- /dev/null
+++ b/t/data/smime/mails/with-text-attachment.eml
@@ -0,0 +1,44 @@
+Date: Fri, 22 Dec 2006 05:24:50 +0300
+From: root at localhost
+X-Mozilla-Draft-Info: internal/draft; vcard=0; receipt=0; uuencode=0
+User-Agent: Thunderbird 1.5.0.9 (X11/20061221)
+MIME-Version: 1.0
+To: sender at test.com
+Subject: test
+Content-Type: application/x-pkcs7-mime; name="smime.p7m"
+Content-Transfer-Encoding: base64
+Content-Disposition: attachment; filename="smime.p7m"
+Content-Description: S/MIME Encrypted Message
+
+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
+qA5aPVUAAAAAAAAAAAAA

commit bf967e87de118ab199ba461268c8c6ed481a912e
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Mon Feb 1 23:50:26 2010 +0300

    tests for SMIME

diff --git a/t/mail/smime/smime-incoming.t b/t/mail/smime/smime-incoming.t
new file mode 100644
index 0000000..ed59ee9
--- /dev/null
+++ b/t/mail/smime/smime-incoming.t
@@ -0,0 +1,237 @@
+#!/usr/bin/perl
+use strict;
+use warnings;
+
+use RT::Test tests => 47;
+
+my $openssl = RT::Test->find_executable('openssl');
+plan skip_all => 'openssl executable is required.'
+    unless $openssl;
+
+use File::Temp;
+use IPC::Run3 'run3';
+use String::ShellQuote 'shell_quote';
+use RT::Tickets;
+use FindBin;
+use Cwd 'abs_path';
+
+# catch any outgoing emails
+RT::Test->set_mail_catcher;
+
+my $keys = RT::Test::get_abs_relocatable_dir(
+    (File::Spec->updir()) x 2,
+    qw(data smime keys),
+);
+
+my $keyring = RT::Test->new_temp_dir(
+    crypt => smime => 'smime_keyring'
+);
+
+RT->Config->Set( Crypt =>
+    Incoming => ['SMIME'],
+    Outgoing => 'SMIME',
+);
+RT->Config->Set( SMIME =>
+    Enable => 1,
+    OutgoingMessagesFormat => 'RFC',
+    Passphrase => {
+        'sender at example.com' => '123456',
+    },
+    OpenSSL => $openssl,
+    Keyring => $keyring,
+);
+RT->Config->Set( GnuPG => Enable => 0 );
+
+RT->Config->Set( 'MailPlugins' => 'Auth::MailFrom', 'Auth::Crypt' );
+
+RT::Test->import_smime_key('sender at example.com');
+
+my $mails = RT::Test::find_relocatable_path( 'data', 'smime', 'mails' );
+
+my ($url, $m) = RT::Test->started_ok;
+# configure key for General queue
+$m->get( $url."?user=root;pass=password" );
+$m->content_like(qr/Logout/, 'we did log in');
+$m->get( $url.'/Admin/Queues/');
+$m->follow_link_ok( {text => 'General'} );
+$m->submit_form( form_number => 3,
+		 fields      => { CorrespondAddress => 'sender at example.com' } );
+
+my $mail = RT::Test->open_mailgate_ok($url);
+print $mail <<EOF;
+From: root\@localhost
+To: rt\@$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 encrypted mail
+    my $buf = '';
+    run3(
+        shell_quote(
+            qw(openssl smime -encrypt  -des3),
+            -from    => 'root at localhost',
+            -to      => 'rt@' . $RT::rtname,
+            -subject => "Encrypted message for queue",
+            File::Spec->catfile( $keys, 'sender at example.com.crt' ),
+        ),
+        \"Subject: test\n\norzzzzzz",
+        \$buf,
+        \*STDERR
+    );
+
+    my ($status, $tid) = RT::Test->send_via_mailgate( $buf );
+    ok !$status, "executed gate";
+
+    my $tick = RT::Ticket->new( $RT::SystemUser );
+    $tick->Load( $tid );
+    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'),
+        'SMIME',
+        'recorded incoming mail that is encrypted'
+    );
+    like( $attach->Content, qr'orz');
+
+    is( $orig->GetHeader('Content-Type'), 'application/x-rt-original-message');
+}
+
+{
+    my $message = RT::Test->file_content([$mails, 'simple-txt-enc.eml']);
+    my ($status, $tid) = RT::Test->send_via_mailgate( $message );
+    ok !$status, "executed gate";
+
+    my $tick = RT::Ticket->new( $RT::SystemUser );
+    $tick->Load( $tid );
+    ok( $tick->Id, "found ticket " . $tick->Id );
+    is( $tick->Subject, 'test', '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'),
+        'SMIME',
+        'recorded incoming mail that is encrypted'
+    );
+    ok( $msg->GetHeader('User-Agent'), 'header is there');
+    like( $attach->Content, qr'test');
+}
+
+{
+    my $message = RT::Test->file_content([$mails, 'with-text-attachment.eml']);
+    my ($status, $tid) = RT::Test->send_via_mailgate( $message );
+    ok !$status, "executed gate";
+
+    my $tick = RT::Ticket->new( $RT::SystemUser );
+    $tick->Load( $tid );
+    ok( $tick->Id, "found ticket " . $tick->Id );
+    is( $tick->Subject, 'test', 'Created the ticket' );
+    my $txn = $tick->Transactions->First;
+    my @attachments = @{ $txn->Attachments->ItemsArrayRef };
+    is( @attachments, 4, '4 attachments: top, two parts and orig' );
+
+    is( $attachments[0]->GetHeader('X-RT-Incoming-Encryption'),
+        'Success',
+        'recorded incoming mail that is encrypted'
+    );
+    ok( $attachments[0]->GetHeader('User-Agent'), 'header is there' );
+    like( $attachments[1]->Content, qr'test' );
+    like( $attachments[2]->Content, qr'text attachment' );
+    is( $attachments[2]->Filename, 'attachment.txt' );
+}
+
+{
+    my $message = RT::Test->file_content([$mails, 'with-bin-attachment.eml']);
+    my ($status, $tid) = RT::Test->send_via_mailgate( $message );
+    ok !$status, "executed gate";
+
+    my $tick = RT::Ticket->new( $RT::SystemUser );
+    $tick->Load( $tid );
+    ok( $tick->Id, "found ticket " . $tick->Id );
+    is( $tick->Subject, 'test', 'Created the ticket' );
+    my $txn = $tick->Transactions->First;
+    my @attachments = @{ $txn->Attachments->ItemsArrayRef };
+    is( @attachments, 4, '4 attachments: top, two parts and orig' );
+
+    is( $attachments[0]->GetHeader('X-RT-Incoming-Encryption'),
+        'Success',
+        'recorded incoming mail that is encrypted'
+    );
+    ok( $attachments[0]->GetHeader('User-Agent'), 'header is there');
+    like( $attachments[1]->Content, qr'test');
+    is( $attachments[2]->Filename, 'attachment.bin' );
+}
+
+{
+    my $buf = '';
+
+    run3(
+        join(
+            ' ',
+            shell_quote(
+                RT->Config->Get('SMIME')->{'OpenSSL'},
+                qw( smime -sign -nodetach -passin pass:123456),
+                -signer => File::Spec->catfile( $keys, 'recipient.crt' ),
+                -inkey  => File::Spec->catfile( $keys, 'recipient.key' ),
+            ),
+            '|',
+            shell_quote(
+                qw(openssl smime -encrypt -des3),
+                -from    => 'root at localhost',
+                -to      => 'rt@' . RT->Config->Get('rtname'),
+                -subject => "Encrypted and signed message for queue",
+                File::Spec->catfile( $keys, 'sender at example.com.crt' ),
+            )),
+            \"Subject: test\n\norzzzzzz",
+            \$buf,
+            \*STDERR
+    );
+
+    my ($status, $tid) = RT::Test->send_via_mailgate( $buf );
+
+    my $tick = RT::Ticket->new( $RT::SystemUser );
+    $tick->Load( $tid );
+    ok( $tick->Id, "found ticket " . $tick->Id );
+    is( $tick->Subject, 'Encrypted and signed 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'
+    );
+    like( $attach->Content, qr'orzzzz');
+}
+
diff --git a/t/mail/smime/smime-outgoing.t b/t/mail/smime/smime-outgoing.t
new file mode 100644
index 0000000..31e6b5b
--- /dev/null
+++ b/t/mail/smime/smime-outgoing.t
@@ -0,0 +1,74 @@
+#!/usr/bin/perl
+use strict;
+use warnings;
+
+use Test::More;
+eval 'use RT::Test; 1'
+    or plan skip_all => 'requires 3.7 to run tests.';
+
+plan tests => 10;
+
+use IPC::Run3 'run3';
+use Cwd 'abs_path';
+use RT::Interface::Email;
+
+use_ok('RT::Crypt::SMIME');
+
+RT::Config->Set( 'MailCommand' => 'sendmail'); # we intercept MIME::Entity::send
+
+RT->Config->Set( 'OpenSSLPath', '/usr/bin/openssl' );
+RT->Config->Set( 'SMIMEKeys', abs_path('testkeys') );
+RT->Config->Set( 'SMIMEPasswords', {'sender at example.com' => '123456'} );
+RT->Config->Set( 'MailPlugins' => 'Auth::MailFrom', 'Auth::SMIME' );
+
+RT::Handle->InsertData('etc/initialdata');
+
+my ($url, $m) = RT::Test->started_ok;
+# configure key for General queue
+$m->get( $url."?user=root;pass=password" );
+$m->content_like(qr/Logout/, 'we did log in');
+$m->get( $url.'/Admin/Queues/');
+$m->follow_link_ok( {text => 'General'} );
+$m->submit_form( form_number => 3,
+		 fields      => { CorrespondAddress => 'sender at example.com' } );
+
+my $user = RT::User->new($RT::SystemUser);
+ok($user->LoadByEmail('root at localhost'), "Loaded user 'root'");
+diag $user->Id;
+ok($user->Load('root'), "Loaded user 'root'");
+is( $user->EmailAddress, 'root at localhost' );
+my $val = $user->FirstCustomFieldValue('PublicKey');
+# XXX: delete if it's already there
+unless (defined $val) {
+    local $/;
+    open my $fh, 'testkeys/recipient.crt' or die $!;
+    $user->AddCustomFieldValue( Field => 'PublicKey', Value => <$fh> );
+    $val = $user->FirstCustomFieldValue('PublicKey');
+}
+
+no warnings 'once';
+local *MIME::Entity::send = sub {
+    my $mime_obj = shift;
+    my ($buf, $err);
+    ok(eval { run3([qw(openssl smime -decrypt -passin pass:123456 -inkey testkeys/recipient.key -recip testkeys/recipient.crt)],
+	 \$mime_obj->as_string, \$buf, \$err) }, 'can decrypt');
+    diag $err if $err;
+    diag "Error code: $?" if $?;
+    like($buf, qr'This message has been automatically generated in response');
+    # XXX: check signature as wel
+};
+
+
+RT::Interface::Email::Gateway( {queue => 1, action => 'correspond',
+			       message => 'From: root at localhost
+To: rt at example.com
+Subject: This is a test of new ticket creation as an unknown user
+
+Blah!
+Foob!'});
+
+my $tickets = RT::Tickets->new($RT::SystemUser);
+$tickets->OrderBy(FIELD => 'id', ORDER => 'DESC');
+$tickets->Limit(FIELD => 'id' ,OPERATOR => '>', VALUE => '0');
+my $tick = $tickets->First();
+ok ($tick->Id, "found ticket ".$tick->Id);

commit 7a86519d0c0039b80dc1c57468987136e037dac3
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Mon Feb 1 23:51:11 2010 +0300

    SMIME crypt implementation

diff --git a/lib/RT/Crypt/SMIME.pm b/lib/RT/Crypt/SMIME.pm
new file mode 100644
index 0000000..fd472fc
--- /dev/null
+++ b/lib/RT/Crypt/SMIME.pm
@@ -0,0 +1,470 @@
+
+use strict;
+use warnings;
+
+package RT::Crypt::SMIME;
+use base 'RT::Crypt::Base';
+
+use IPC::Run3 0.036 'run3';
+use String::ShellQuote 'shell_quote';
+use RT::Util 'safe_run_child';
+
+{ my $cache = shift;
+sub OpenSSLPath {
+    return $cache ||= RT->Config->Get('SMIME')->{'OpenSSL'};
+} }
+
+sub SignEncrypt {
+    my $self = shift;
+    my %args = (
+        Entity => undef,
+
+        Sign => 1,
+        Signer => undef,
+        Passphrase => undef,
+
+        Encrypt => 1,
+        Recipients => undef,
+
+        @_
+    );
+
+    my $entity = $args{'Entity'};
+
+    if ( $args{'Sign'} && !defined $args{'Passphrase'} ) {
+        $args{'Passphrase'} = $self->GetPassphrase( Address => $args{'Signer'} );
+    }
+
+    my %res = (exit_code => 0, status => []);
+
+    my @addresses =
+        map $_->address,
+        Email::Address->parse($_),
+        grep defined && length,
+        map $entity->head->get($_),
+        qw(To Cc Bcc);
+
+    my @keys;
+    foreach my $address ( @addresses ) {
+        $RT::Logger->debug( "Considering encrypting message to " . $address );
+        my $user = RT::User->new( $RT::SystemUser );
+        $user->LoadByEmail( $address );
+
+        my $key;
+        $key = $user->FirstCustomFieldValue('PublicKey') if $user->id;
+        unless ( $key ) {
+            $res{'exit_code'} = 1;
+            my $reason = 'Key not found';
+            push @{ $res{'status'} }, {
+                Operation  => 'RecipientsCheck',
+                Status     => 'ERROR',
+                Message    => "Recipient '$address' is unusable, the reason is '$reason'",
+                Recipient  => $address,
+                Reason     => $reason,
+            };
+            next;
+        }
+
+        my $expire = $self->GetKeyExpiration( $user );
+        unless ( $expire ) {
+            # we continue here as it's most probably a problem with the key,
+            # so later during encryption we'll get verbose errors
+            $RT::Logger->error(
+                "Trying to send an encrypted message to ". $address
+                .", but we couldn't get expiration date of the key."
+            );
+        }
+        elsif ( $expire->Diff( time ) < 0 ) {
+            $res{'exit_code'} = 1;
+            my $reason = 'Key expired';
+            push @{ $res{'status'} }, {
+                Operation  => 'RecipientsCheck',
+                Status     => 'ERROR',
+                Message    => "Recipient '$address' is unusable, the reason is '$reason'",
+                Recipient  => $address,
+                Reason     => $reason,
+            };
+            next;
+        }
+        push @keys, $key;
+    }
+    return %res if $res{'exit_code'};
+
+    foreach my $key ( @keys ) {
+        my $key_file = File::Temp->new;
+        print $key_file $key;
+        $key = $key_file;
+    }
+
+    my $opts = RT->Config->Get('SMIME');
+
+    $entity->make_multipart('mixed', Force => 1);
+    my ($buf, $err) = ('', '');
+    {
+        local $ENV{'SMIME_PASS'};
+        
+        my @command;
+        if ( $args{'Sign'} ) {
+            $ENV{'SMIME_PASS'} = $args{'Passphrase'};
+            push @command, join ' ', shell_quote(
+                $self->OpenSSLPath, qw(smime -sign -passin env:SMIME_PASS),
+                -signer => $opts->{'Keyring'} .'/'. $args{'Signer'} .'.pem',
+                -inkey  => $opts->{'Keyring'} .'/'. $args{'Signer'} .'.pem',
+            );
+        }
+        if ( $args{'Encrypt'} ) {
+            push @command, join ' ', shell_quote(
+                $self->OpenSSLPath, qw(smime -encrypt -des3),
+                map { $_->filename } @keys
+            );
+        }
+
+        local $SIG{'CHLD'} = 'DEFAULT';
+        safe_run_child { run3(
+            join( ' | ', @command ),
+            \$entity->parts(0)->stringify,
+            \$buf, \$err
+        ) };
+    }
+    $RT::Logger->debug( "openssl stderr: " . $err ) if length $err;
+
+    my $tmpdir = File::Temp::tempdir( TMPDIR => 1, CLEANUP => 1 );
+    my $parser = MIME::Parser->new();
+    $parser->output_dir($tmpdir);
+    my $newmime = $parser->parse_data($buf);
+
+    $entity->parts([$newmime]);
+    $entity->make_singlepart;
+
+    return %res;
+}
+
+sub VerifyDecrypt {
+    my $self = shift;
+    my %args = (
+        Info      => undef,
+        Detach    => 1,
+        SetStatus => 1,
+        AddStatus => 0,
+        @_
+    );
+
+    my %res;
+
+    my $item = $args{'Info'};
+    if ( $item->{'Type'} eq 'signed' ) {
+        my $status_on;
+        if ( $item->{'Format'} eq 'RFC3156' ) {
+            $status_on = $item->{'Top'};
+            %res = $self->VerifyRFC3156( %$item, SetStatus => $args{'SetStatus'} );
+            if ( $args{'Detach'} ) {
+                $item->{'Top'}->parts( [ $item->{'Data'} ] );
+                $item->{'Top'}->make_singlepart;
+            }
+        }
+        elsif ( $item->{'Format'} eq 'RFC3851' ) {
+            $status_on = $item->{'Data'};
+            %res = $self->VerifyRFC3851( %$item, SetStatus => $args{'SetStatus'} );
+        }
+        else {
+            die "Unknow signature format. Shouldn't ever happen.";
+        }
+        if ( $args{'SetStatus'} || $args{'AddStatus'} ) {
+            my $method = $args{'AddStatus'} ? 'add' : 'set';
+            $status_on->head->$method(
+                'X-RT-SMIME-Status' => $res{'status'}
+            );
+        }
+    } elsif ( $item->{'Type'} eq 'encrypted' ) {
+        %res = $self->DecryptRFC3851( %args, %$item );
+        if ( $args{'SetStatus'} || $args{'AddStatus'} ) {
+            my $method = $args{'AddStatus'} ? 'add' : 'set';
+            $item->{'Data'}->head->$method(
+                'X-RT-SMIME-Status' => $res{'status'}
+            );
+        }
+    } else {
+        die "Unknow type '". $item->{'Type'} ."' of protected item";
+    }
+    return %res;
+}
+
+sub VerifyRFC3851 {
+    my $self = shift;
+    my %args = (Data => undef, Queue => undef, @_ );
+
+    my $msg = $args{'Data'}->as_string;
+
+    my %res;
+    my $buf;
+    {
+        local $SIG{CHLD} = 'DEFAULT';
+        my $cmd = join( ' ', shell_quote(
+            $self->OpenSSLPath, qw(smime -verify -noverify),
+        ) );
+        safe_run_child { run3( $cmd, \$msg, \$buf, \$res{'stderr'} ) };
+        $res{'exit_code'} = $?;
+    }
+    if ( $res{'exit_code'} ) {
+        $res{'message'} = "openssl exitted with error code ". ($? >> 8)
+            ." and error: $res{stderr}";
+        return %res;
+    }
+
+    my $res_entity = _extract_msg_from_buf( \$buf );
+    unless ( $res_entity ) {
+        $res{'exit_code'} = 1;
+        $res{'message'} = "verified message, but couldn't parse result";
+        return %res;
+    }
+    $res_entity->make_multipart( 'mixed', Force => 1 );
+
+    $args{'Data'}->make_multipart( 'mixed', Force => 1 );
+    $args{'Data'}->parts([ $res_entity->parts ]);
+    $args{'Data'}->make_singlepart;
+
+    $res{'status'} =
+        "Operation: Verify\nStatus: DONE\n"
+        ."Message: The signature is good\n"
+    ;
+
+    return %res;
+}
+
+sub DecryptRFC3851 {
+    my $self = shift;
+    my %args = (Data => undef, Queue => undef, @_ );
+
+    my $msg = $args{'Data'}->as_string;
+
+    my $action = 'correspond';
+    $action = 'comment' if grep defined && $_ eq 'comment', @{ $args{'Actions'}||[] };
+
+    my $address = $action eq 'correspond'
+        ? $args{'Queue'}->CorrespondAddress || RT->Config->Get('CorrespondAddress')
+        : $args{'Queue'}->CommentAddress    || RT->Config->Get('CommentAddress');
+
+    my %res;
+    my $buf;
+    {
+        local $ENV{SMIME_PASS} = $self->GetPassphrase( Address => $address );
+        local $SIG{CHLD} = 'DEFAULT';
+        my $cmd = join( ' ', shell_quote(
+            $self->OpenSSLPath,
+            qw(smime -decrypt -passin env:SMIME_PASS),
+            -recip => RT->Config->Get('SMIME')->{'Keyring'} .'/'. $address .'.pem',
+        ) );
+        safe_run_child { run3( $cmd, \$msg, \$buf, \$res{'stderr'} ) };
+        $res{'exit_code'} = $?;
+    }
+    if ( $res{'exit_code'} ) {
+        $res{'message'} = "openssl exitted with error code ". ($? >> 8)
+            ." and error: $res{stderr}";
+        return %res;
+    }
+
+    my $res_entity = _extract_msg_from_buf( \$buf );
+    $res_entity->make_multipart( 'mixed', Force => 1 );
+
+    $args{'Data'}->make_multipart( 'mixed', Force => 1 );
+    $args{'Data'}->parts([ $res_entity->parts ]);
+    $args{'Data'}->make_singlepart;
+
+    $res{'status'} =
+        "Operation: Decrypt\nStatus: DONE\n"
+        ."Message: Decryption process succeeded\n"
+        ."EncryptedTo: $address\n";
+
+    return %res;
+}
+
+sub ParseStatus {
+    my $self = shift;
+    my $status = shift;
+    return () unless $status;
+
+    my @status = split /\n\n/, $status;
+    foreach my $block ( @status ) {
+        chomp $block;
+        $block = { map { s/^\s+//; s/\s+$//; $_ } map split(/:/, $_, 2), split /\n+/, $block };
+    }
+    foreach my $block ( grep $_->{'EncryptedTo'}, @status ) {
+        $block->{'EncryptedTo'} = [{
+            EmailAddress => $block->{'EncryptedTo'},  
+        }];
+    }
+
+    return @status;
+}
+
+sub _extract_msg_from_buf {
+    my $buf = shift;
+    my $rtparser = RT::EmailParser->new();
+    my $parser   = MIME::Parser->new();
+    $rtparser->_SetupMIMEParser($parser);
+    $parser->output_to_core(1);
+    unless ( $rtparser->{'entity'} = $parser->parse_data($$buf) ) {
+        $RT::Logger->crit("Couldn't parse MIME stream and extract the submessages");
+
+        # Try again, this time without extracting nested messages
+        $parser->extract_nested_messages(0);
+        unless ( $rtparser->{'entity'} = $parser->parse_data($$buf) ) {
+            $RT::Logger->crit("couldn't parse MIME stream");
+            return (undef);
+        }
+    }
+    $rtparser->_PostProcessNewEntity;
+    return $rtparser->Entity;
+}
+
+
+sub CheckIfProtected {
+    my $self = shift;
+    my %args = ( Entity => undef, @_ );
+
+    my $entity = $args{'Entity'};
+
+    my $type = $entity->effective_type;
+    if ( $type =~ m{^application/(?:x-)?pkcs7-mime$} || $type eq 'application/octet-stream' ) {
+        # RFC3851 ch.3.9 variant 1 and 3
+
+        my $security_type;
+
+        my $smime_type = $entity->head->mime_attr('Content-Type.smime-type');
+        if ( $smime_type ) { # it's optional according to RFC3851
+            if ( $smime_type eq 'enveloped-data' ) {
+                $security_type = 'encrypted';
+            }
+            elsif ( $smime_type eq 'signed-data' ) {
+                $security_type = 'signed';
+            }
+            elsif ( $smime_type eq 'certs-only' ) {
+                $security_type = 'certificate management';
+            }
+            elsif ( $smime_type eq 'compressed-data' ) {
+                $security_type = 'compressed';
+            }
+            else {
+                $security_type = $smime_type;
+            }
+        }
+
+        unless ( $security_type ) {
+            my $fname = $entity->head->recommended_filename || '';
+            if ( $fname =~ /\.p7([czsm])$/ ) {
+                my $type_char = $1;
+                if ( $type_char eq 'm' ) {
+                    # RFC3851, ch3.4.2
+                    # it can be both encrypted and signed
+                    $security_type = 'encrypted';
+                }
+                elsif ( $type_char eq 's' ) {
+                    # RFC3851, ch3.4.3, multipart/signed, XXX we should never be here
+                    # unless message is changed by some gateway
+                    $security_type = 'signed';
+                }
+                elsif ( $type_char eq 'c' ) {
+                    # RFC3851, ch3.7
+                    $security_type = 'certificate management';
+                }
+                elsif ( $type_char eq 'z' ) {
+                    # RFC3851, ch3.5
+                    $security_type = 'compressed';
+                }
+            }
+        }
+        return () if !$security_type && $type eq 'application/octet-stream';
+
+        return (
+            Type   => $security_type,
+            Format => 'RFC3851',
+            Data   => $entity,
+        );
+    }
+    elsif ( $type eq 'multipart/signed' ) {
+        # RFC3156, multipart/signed
+        # RFC3851, ch.3.9 variant 2
+
+        unless ( $entity->parts == 2 ) {
+            $RT::Logger->error( "Encrypted or signed entity must has two subparts. Skipped" );
+            return ();
+        }
+
+        my $protocol = $entity->head->mime_attr( 'Content-Type.protocol' );
+        unless ( $protocol ) {
+            $RT::Logger->error( "Entity is '$type', but has no protocol defined. Skipped" );
+            return ();
+        }
+
+        unless ( $protocol eq 'application/pgp-signature' ) {
+            $RT::Logger->info( "Skipping protocol '$protocol', only 'application/pgp-signature' is supported" );
+            return ();
+        }
+        $RT::Logger->debug("Found part signed according to RFC3156");
+        return (
+            Type      => 'signed',
+            Format    => 'RFC3156',
+            Top       => $entity,
+            Data      => $entity->parts(0),
+            Signature => $entity->parts(1),
+        );
+    }
+    return ();
+}
+
+sub KeyExpirationDate {
+    my $self = shift;
+    my %args = (@_);
+
+    my $user = $args{'User'};
+
+    my $key_obj = $user->CustomFieldValues('PublicKey')->First;
+    unless ( $key_obj ) {
+        $RT::Logger->warn('User #'. $user->id .' has no SMIME key');
+        return;
+    }
+
+    my $attr = $user->FirstAttribute('SMIMEKeyNotAfter');
+    if ( $attr and my $date_str = $attr->Content
+         and $key_obj->LastUpdatedObj->Unix < $attr->LastUpdatedObj->Unix )
+    {
+        my $date = RT::Date->new( $RT::SystemUser );
+        $date->Set( Format => 'unknown', Value => $attr->Content );
+        return $date;
+    }
+    $RT::Logger->debug('Expiration date of SMIME key is not up to date');
+
+    my $key = $key_obj->Content;
+
+    my ($buf, $err) = ('', '');
+    {
+        local $ENV{SMIME_PASS} = '123456';
+        safe_run3(
+            join( ' ', shell_quote( $self->OpenSSLPath, qw(x509 -noout -dates) ) ),
+            \$key, \$buf, \$err
+        );
+    }
+    $RT::Logger->debug( "openssl stderr: " . $err ) if length $err;
+
+    my ($date_str) = ($buf =~ /^notAfter=(.*)$/m);
+    return unless $date_str;
+
+    $RT::Logger->debug( "smime key expiration date is $date_str" );
+    $user->SetAttribute(
+        Name => 'SMIMEKeyNotAfter',
+        Description => 'SMIME key expiration date',
+        Content => $date_str,
+    );
+    my $date = RT::Date->new( $RT::SystemUser );
+    $date->Set( Format => 'unknown', Value => $date_str );
+    return $date;
+}
+
+sub GetPassphrase {
+    my $self = shift;
+    my %args = (Address => undef, @_);
+    $args{'Address'} = '' unless defined $args{'Address'};
+    return RT->Config->Get('SMIME')->{'Passphrase'}->{ $args{'Address'} };
+}
+
+1;

-----------------------------------------------------------------------


More information about the Rt-commit mailing list