import rt 3.8.7
[freeside.git] / rt / lib / RT / Crypt / GnuPG.pm
diff --git a/rt/lib/RT/Crypt/GnuPG.pm b/rt/lib/RT/Crypt/GnuPG.pm
new file mode 100644 (file)
index 0000000..5581df1
--- /dev/null
@@ -0,0 +1,2450 @@
+# BEGIN BPS TAGGED BLOCK {{{
+# 
+# COPYRIGHT:
+# 
+# This software is Copyright (c) 1996-2009 Best Practical Solutions, LLC
+#                                          <jesse@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 }}}
+
+use strict;
+use warnings;
+
+package RT::Crypt::GnuPG;
+
+use IO::Handle;
+use GnuPG::Interface;
+use RT::EmailParser ();
+use RT::Util 'safe_run_child';
+
+=head1 NAME
+
+RT::Crypt::GnuPG - encrypt/decrypt and sign/verify email messages with the GNU Privacy Guard (GPG)
+
+=head1 DESCRIPTION
+
+This module provides support for encryption and signing of outgoing messages, 
+as well as the decryption and verification of incoming email.
+
+=head1 CONFIGURATION
+
+You can control the configuration of this subsystem from RT's configuration file.
+Some options are available via the web interface, but to enable this functionality, you
+MUST start in the configuration file.
+
+There are two hashes, GnuPG and GnuPGOptions in the configuration file. The 
+first one controls RT specific options. It enables you to enable/disable facility 
+or change the format of messages. The second one is a hash with options for the 
+'gnupg' utility. You can use it to define a keyserver, enable auto-retrieval keys 
+and set almost any option 'gnupg' supports on your system.
+
+=head2 %GnuPG
+
+=head3 Enabling GnuPG
+
+Set to true value to enable this subsystem:
+
+    Set( %GnuPG,
+        Enable => 1,
+        ... other options ...
+    );
+
+However, note that you B<must> add the 'Auth::GnuPG' email filter to enable
+the handling of incoming encrypted/signed messages.
+
+=head3 Format of outgoing messages
+
+Format of outgoing messages can be controlled using the 'OutgoingMessagesFormat'
+option in the RT config:
+
+    Set( %GnuPG,
+        ... other options ...
+        OutgoingMessagesFormat => 'RFC',
+        ... other options ...
+    );
+
+or
+
+    Set( %GnuPG,
+        ... other options ...
+        OutgoingMessagesFormat => 'Inline',
+        ... other options ...
+    );
+
+This framework implements two formats of signing and encrypting of email messages:
+
+=over
+
+=item RFC
+
+This format is also known as GPG/MIME and described in RFC3156 and RFC1847.
+Technique described in these RFCs is well supported by many mail user
+agents (MUA), but some MUAs support only inline signatures and encryption,
+so it's possible to use inline format (see below).
+
+=item Inline
+
+This format doesn't take advantage of MIME, but some mail clients do
+not support GPG/MIME.
+
+We sign text parts using clear signatures. For each attachments another
+attachment with a signature is added with '.sig' extension.
+
+Encryption of text parts is implemented using inline format, other parts
+are replaced with attachments with the filename extension '.pgp'.
+
+This format is discouraged because modern mail clients typically don't support
+it well.
+
+=back
+
+=head3 Encrypting data in the database
+
+You can allow users to encrypt data in the database using
+option C<AllowEncryptDataInDB>. By default it's disabled.
+Users must have rights to see and modify tickets to use
+this feature.
+
+=head2 %GnuPGOptions
+
+Use this hash to set options of the 'gnupg' program. You can define almost any
+option you want which  gnupg supports, but never try to set options which
+change output format or gnupg's commands, such as --sign (command),
+--list-options (option) and other.
+
+Some GnuPG options take arguments while others take none. (Such as  --use-agent).
+For options without specific value use C<undef> as hash value.
+To disable these option just comment them out or delete them from the hash
+
+    Set(%GnuPGOptions,
+        'option-with-value' => 'value',
+        'enabled-option-without-value' => undef,
+        # 'commented-option' => 'value or undef',
+    );
+
+B<NOTE> that options may contain '-' character and such options B<MUST> be
+quoted, otherwise you can see quite cryptic error 'gpg: Invalid option "--0"'.
+
+=over
+
+=item --homedir
+
+The GnuPG home directory, by default it is set to F</opt/rt3/var/data/gpg>.
+
+You can manage this data with the 'gpg' commandline utility 
+using the GNUPGHOME environment variable or --homedir option. 
+Other utilities may be used as well.
+
+In a standard installation, access to this directory should be granted to
+the web server user which is running RT's web interface, but if you're running
+cronjobs or other utilities that access RT directly via API and may generate
+encrypted/signed notifications then the users you execute these scripts under
+must have access too. 
+
+However, granting access to the dir to many users makes your setup less secure,
+some features, such as auto-import of keys, may not be available if you do not.
+To enable this features and suppress warnings about permissions on
+the dir use --no-permission-warning.
+
+=item --digest-algo
+
+This option is required in advance when RFC format for outgoing messages is
+used. We can not get default algorithm from gpg program so RT uses 'SHA1' by
+default. You may want to override it. You can use MD5, SHA1, RIPEMD160,
+SHA256 or other, however use `gpg --version` command to get information about
+supported algorithms by your gpg. These algorithms are listed as hash-functions.
+
+=item --use-agent
+
+This option lets you use GPG Agent to cache the passphrase of RT's key. See
+L<http://www.gnupg.org/documentation/manuals/gnupg/Invoking-GPG_002dAGENT.html>
+for information about GPG Agent.
+
+=item --passphrase
+
+This option lets you set the passphrase of RT's key directly. This option is
+special in that it isn't passed directly to GPG, but is put into a file that
+GPG then reads (which is more secure). The downside is that anyone who has read
+access to your RT_SiteConfig.pm file can see the passphrase, thus we recommend
+the --use-agent option instead.
+
+=item other
+
+Read `man gpg` to get list of all options this program support.
+
+=back
+
+=head2 Per-queue options
+
+Using the web interface it's possible to enable signing and/or encrypting by
+default. As an administrative user of RT, open 'Configuration' then 'Queues',
+and select a queue. On the page you can see information about the queue's keys 
+at the bottom and two checkboxes to choose default actions.
+
+As well, encryption is enabled for autoreplies and other notifications when
+an encypted message enters system via mailgate interface even if queue's
+option is disabled.
+
+=head2 Handling incoming messages
+
+To enable handling of encrypted and signed message in the RT you should add
+'Auth::GnuPG' mail plugin.
+
+    Set(@MailPlugins, 'Auth::MailFrom', 'Auth::GnuPG', ...other filter...);
+
+See also `perldoc lib/RT/Interface/Email/Auth/GnuPG.pm`.
+
+=head2 Errors handling
+
+There are several global templates created in the database by default. RT
+uses these templates to send error messages to users or RT's owner. These 
+templates have 'Error:' or 'Error to RT owner:' prefix in the name. You can 
+adjust the text of the messages using the web interface.
+
+Note that C<$TicketObj>, C<$TransactionObj> and other variable usually available
+in RT's templates are not available in these templates, but each template
+used for errors reporting has set of available data structures you can use to
+build better messages. See default templates and descriptions below.
+
+As well, you can disable particular notification by deleting content of
+a template. You can delete a template too, but in this case you'll see
+error messages in the logs when RT can not load template you've deleted.
+
+=head3 Problems with public keys
+
+Template 'Error: public key' is used to inform the user that RT has problems with
+his public key and won't be able to send him encrypted content. There are several 
+reasons why RT can't use a key. However, the actual reason is not sent to the user, 
+but sent to RT owner using 'Error to RT owner: public key'.
+
+The possible reasons: "Not Found", "Ambiguous specification", "Wrong
+key usage", "Key revoked", "Key expired", "No CRL known", "CRL too
+old", "Policy mismatch", "Not a secret key", "Key not trusted" or
+"No specific reason given".
+
+Due to limitations of GnuPG, it's impossible to encrypt to an untrusted key,
+unless 'always trust' mode is enabled.
+
+In the 'Error: public key' template there are a few additional variables available:
+
+=over 4
+
+=item $Message - user friendly error message
+
+=item $Reason - short reason as listed above
+
+=item $Recipient - recipient's identification
+
+=item $AddressObj - L<Email::Address> object containing recipient's email address
+
+=back
+
+A message can have several invalid recipients, to avoid sending many emails
+to the RT owner the system sends one message to the owner, grouped by
+recipient. In the 'Error to RT owner: public key' template a C<@BadRecipients>
+array is available where each element is a hash reference that describes one
+recipient using the same fields as described above. So it's something like:
+
+    @BadRecipients = (
+        { Message => '...', Reason => '...', Recipient => '...', ...},
+        { Message => '...', Reason => '...', Recipient => '...', ...},
+        ...
+    )
+
+=head3 Private key doesn't exist
+
+Template 'Error: no private key' is used to inform the user that
+he sent an encrypted email, but we have no private key to decrypt
+it.
+
+In this template C<$Message> object of L<MIME::Entity> class
+available. It's the message RT received.
+
+=head3 Invalid data
+
+Template 'Error: bad GnuPG data' used to inform the user that a
+message he sent has invalid data and can not be handled.
+
+There are several reasons for this error, but most of them are data
+corruption or absence of expected information.
+
+In this template C<@Messages> array is available and contains list
+of error messages.
+
+=head1 FOR DEVELOPERS
+
+=head2 Documentation and references
+
+* RFC1847 - Security Multiparts for MIME: Multipart/Signed and Multipart/Encrypted.
+Describes generic MIME security framework, "mulitpart/signed" and "multipart/encrypted"
+MIME types.
+
+* RFC3156 - MIME Security with Pretty Good Privacy (PGP),
+updates RFC2015.
+
+=cut
+
+# gnupg options supported by GnuPG::Interface
+# other otions should be handled via extra_args argument
+my %supported_opt = map { $_ => 1 } qw(
+       always_trust
+       armor
+       batch
+       comment
+       compress_algo
+       default_key
+       encrypt_to
+       extra_args
+       force_v3_sigs
+       homedir
+       logger_fd
+       no_greeting
+       no_options
+       no_verbose
+       openpgp
+       options
+       passphrase_fd
+       quiet
+       recipients
+       rfc1991
+       status_fd
+       textmode
+       verbose
+);
+
+# DEV WARNING: always pass all STD* handles to GnuPG interface even if we don't
+# need them, just pass 'new IO::Handle' and then close it after safe_run_child.
+# we don't want to leak anything into FCGI/Apache/MP handles, this break things.
+# So code should look like:
+#        my $handles = GnuPG::Handles->new(
+#            stdin  => ($handle{'stdin'}  = new IO::Handle),
+#            stdout => ($handle{'stdout'} = new IO::Handle),
+#            stderr => ($handle{'stderr'}  = new IO::Handle),
+#            ...
+#        );
+
+=head2 SignEncrypt Entity => MIME::Entity, [ Encrypt => 1, Sign => 1, ... ]
+
+Signs and/or encrypts an email message with GnuPG utility.
+
+=over
+
+=item Signing
+
+During signing you can pass C<Signer> argument to set key we sign with this option
+overrides gnupg's C<default-key> option. If C<Signer> argument is not provided
+then address of a message sender is used.
+
+As well you can pass C<Passphrase>, but if value is undefined then L</GetPassphrase>
+called to get it.
+
+=item Encrypting
+
+During encryption you can pass a C<Recipients> array, otherwise C<To>, C<Cc> and
+C<Bcc> fields of the message are used to fetch the list.
+
+=back
+
+Returns a hash with the following keys:
+
+* exit_code
+* error
+* logger
+* status
+* message
+
+=cut
+
+sub SignEncrypt {
+    my %args = (@_);
+
+    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 );
+    } else {
+        return SignEncryptRFC3156( %args );
+    }
+}
+
+sub SignEncryptRFC3156 {
+    my %args = (
+        Entity => undef,
+
+        Sign => 1,
+        Signer => undef,
+        Passphrase => undef,
+
+        Encrypt => 1,
+        Recipients => undef,
+
+        @_
+    );
+
+    my $gnupg = new GnuPG::Interface;
+    my %opt = RT->Config->Get('GnuPGOptions');
+
+    # handling passphrase in GnuPGOptions
+    $args{'Passphrase'} = delete $opt{'passphrase'}
+        if !defined $args{'Passphrase'};
+
+    $opt{'digest-algo'} ||= 'SHA1';
+    $opt{'default_key'} = $args{'Signer'}
+        if $args{'Sign'} && $args{'Signer'};
+    $gnupg->options->hash_init(
+        _PrepareGnuPGOptions( %opt ),
+        armor => 1,
+        meta_interactive => 0,
+    );
+
+    my $entity = $args{'Entity'};
+
+    if ( $args{'Sign'} && !defined $args{'Passphrase'} ) {
+        $args{'Passphrase'} = GetPassphrase( Address => $args{'Signer'} );
+    }
+
+    my %res;
+    if ( $args{'Sign'} && !$args{'Encrypt'} ) {
+        # required by RFC3156(Ch. 5) and RFC1847(Ch. 2.1)
+        foreach ( grep !$_->is_multipart, $entity->parts_DFS ) {
+            my $tenc = $_->head->mime_encoding;
+            unless ( $tenc =~ m/^(?:7bit|quoted-printable|base64)$/i ) {
+                $_->head->mime_attr( 'Content-Transfer-Encoding'
+                    => $_->effective_type =~ m{^text/}? 'quoted-printable': 'base64'
+                );
+            }
+        }
+
+        my ($handles, $handle_list) = _make_gpg_handles(stdin =>IO::Handle::CRLF->new );
+        my %handle = %$handle_list;
+
+        $gnupg->passphrase( $args{'Passphrase'} );
+
+        eval {
+            local $SIG{'CHLD'} = 'DEFAULT';
+            my $pid = safe_run_child { $gnupg->detach_sign( handles => $handles ) };
+            $entity->make_multipart( 'mixed', Force => 1 );
+            {
+                local $SIG{'PIPE'} = 'IGNORE';
+                $entity->parts(0)->print( $handle{'stdin'} );
+                close $handle{'stdin'};
+            }
+            waitpid $pid, 0;
+        };
+        my $err = $@;
+        my @signature = readline $handle{'stdout'};
+        close $handle{'stdout'};
+
+        $res{'exit_code'} = $?;
+        foreach ( qw(stderr logger status) ) {
+            $res{$_} = do { local $/; readline $handle{$_} };
+            delete $res{$_} unless $res{$_} && $res{$_} =~ /\S/s;
+            close $handle{$_};
+        }
+        $RT::Logger->debug( $res{'status'} ) if $res{'status'};
+        $RT::Logger->warning( $res{'stderr'} ) if $res{'stderr'};
+        $RT::Logger->error( $res{'logger'} ) if $res{'logger'} && $?;
+        if ( $err || $res{'exit_code'} ) {
+            $res{'message'} = $err? $err : "gpg exitted with error code ". ($res{'exit_code'} >> 8);
+            return %res;
+        }
+
+        # setup RFC1847(Ch.2.1) requirements
+        my $protocol = 'application/pgp-signature';
+        $entity->head->mime_attr( 'Content-Type' => 'multipart/signed' );
+        $entity->head->mime_attr( 'Content-Type.protocol' => $protocol );
+        $entity->head->mime_attr( 'Content-Type.micalg'   => 'pgp-'. lc $opt{'digest-algo'} );
+        $entity->attach(
+            Type        => $protocol,
+            Disposition => 'inline',
+            Data        => \@signature,
+            Encoding    => '7bit',
+        );
+    }
+    if ( $args{'Encrypt'} ) {
+        my %seen;
+        $gnupg->options->push_recipients( $_ ) foreach 
+            map UseKeyForEncryption($_) || $_,
+            grep !$seen{ $_ }++, map $_->address,
+            map Email::Address->parse( $entity->head->get( $_ ) ),
+            qw(To Cc Bcc);
+
+        my ($tmp_fh, $tmp_fn) = File::Temp::tempfile( UNLINK => 1 );
+        binmode $tmp_fh, ':raw';
+
+        my ($handles, $handle_list) = _make_gpg_handles(stdout => $tmp_fh);
+        my %handle = %$handle_list;
+        $handles->options( 'stdout'  )->{'direct'} = 1;
+        $gnupg->passphrase( $args{'Passphrase'} ) if $args{'Sign'};
+
+        eval {
+            local $SIG{'CHLD'} = 'DEFAULT';
+            my $pid = safe_run_child { $args{'Sign'}
+                ? $gnupg->sign_and_encrypt( handles => $handles )
+                : $gnupg->encrypt( handles => $handles ) };
+            $entity->make_multipart( 'mixed', Force => 1 );
+            {
+                local $SIG{'PIPE'} = 'IGNORE';
+                $entity->parts(0)->print( $handle{'stdin'} );
+                close $handle{'stdin'};
+            }
+            waitpid $pid, 0;
+        };
+
+        $res{'exit_code'} = $?;
+        foreach ( qw(stderr logger status) ) {
+            $res{$_} = do { local $/; readline $handle{$_} };
+            delete $res{$_} unless $res{$_} && $res{$_} =~ /\S/s;
+            close $handle{$_};
+        }
+        $RT::Logger->debug( $res{'status'} ) if $res{'status'};
+        $RT::Logger->warning( $res{'stderr'} ) if $res{'stderr'};
+        $RT::Logger->error( $res{'logger'} ) if $res{'logger'} && $?;
+        if ( $@ || $? ) {
+            $res{'message'} = $@? $@: "gpg exited with error code ". ($? >> 8);
+            return %res;
+        }
+
+        my $protocol = 'application/pgp-encrypted';
+        $entity->parts([]);
+        $entity->head->mime_attr( 'Content-Type' => 'multipart/encrypted' );
+        $entity->head->mime_attr( 'Content-Type.protocol' => $protocol );
+        $entity->attach(
+            Type        => $protocol,
+            Disposition => 'inline',
+            Data        => ['Version: 1',''],
+            Encoding    => '7bit',
+        );
+        $entity->attach(
+            Type        => 'application/octet-stream',
+            Disposition => 'inline',
+            Path        => $tmp_fn,
+            Filename    => '',
+            Encoding    => '7bit',
+        );
+        $entity->parts(-1)->bodyhandle->{'_dirty_hack_to_save_a_ref_tmp_fh'} = $tmp_fh;
+    }
+    return %res;
+}
+
+sub SignEncryptInline {
+    my %args = ( @_ );
+
+    my $entity = $args{'Entity'};
+
+    my %res;
+    $entity->make_singlepart;
+    if ( $entity->is_multipart ) {
+        foreach ( $entity->parts ) {
+            %res = SignEncryptInline( @_, Entity => $_ );
+            return %res if $res{'exit_code'};
+        }
+        return %res;
+    }
+
+    return _SignEncryptTextInline( @_ )
+        if $entity->effective_type =~ /^text\//i;
+
+    return _SignEncryptAttachmentInline( @_ );
+}
+
+sub _SignEncryptTextInline {
+    my %args = (
+        Entity => undef,
+
+        Sign => 1,
+        Signer => undef,
+        Passphrase => undef,
+
+        Encrypt => 1,
+        Recipients => undef,
+
+        @_
+    );
+    return unless $args{'Sign'} || $args{'Encrypt'};
+
+    my $gnupg = new GnuPG::Interface;
+    my %opt = RT->Config->Get('GnuPGOptions');
+
+    # handling passphrase in GnupGOptions
+    $args{'Passphrase'} = delete $opt{'passphrase'}
+        if !defined($args{'Passphrase'});
+
+    $opt{'digest-algo'} ||= 'SHA1';
+    $opt{'default_key'} = $args{'Signer'}
+        if $args{'Sign'} && $args{'Signer'};
+    $gnupg->options->hash_init(
+        _PrepareGnuPGOptions( %opt ),
+        armor => 1,
+        meta_interactive => 0,
+    );
+
+    if ( $args{'Sign'} && !defined $args{'Passphrase'} ) {
+        $args{'Passphrase'} = GetPassphrase( Address => $args{'Signer'} );
+    }
+
+    if ( $args{'Encrypt'} ) {
+        $gnupg->options->push_recipients( $_ ) foreach 
+            map UseKeyForEncryption($_) || $_,
+            @{ $args{'Recipients'} || [] };
+    }
+
+    my %res;
+
+    my ($tmp_fh, $tmp_fn) = File::Temp::tempfile( UNLINK => 1 );
+    binmode $tmp_fh, ':raw';
+
+    my ($handles, $handle_list) = _make_gpg_handles(stdout => $tmp_fh);
+    my %handle = %$handle_list;
+
+    $handles->options( 'stdout'  )->{'direct'} = 1;
+    $gnupg->passphrase( $args{'Passphrase'} ) if $args{'Sign'};
+
+    my $entity = $args{'Entity'};
+    eval {
+        local $SIG{'CHLD'} = 'DEFAULT';
+        my $method = $args{'Sign'} && $args{'Encrypt'}
+            ? 'sign_and_encrypt'
+            : ($args{'Sign'}? 'clearsign': 'encrypt');
+        my $pid = safe_run_child { $gnupg->$method( handles => $handles ) };
+        {
+            local $SIG{'PIPE'} = 'IGNORE';
+            $entity->bodyhandle->print( $handle{'stdin'} );
+            close $handle{'stdin'};
+        }
+        waitpid $pid, 0;
+    };
+    $res{'exit_code'} = $?;
+    my $err = $@;
+
+    foreach ( qw(stderr logger status) ) {
+        $res{$_} = do { local $/; readline $handle{$_} };
+        delete $res{$_} unless $res{$_} && $res{$_} =~ /\S/s;
+        close $handle{$_};
+    }
+    $RT::Logger->debug( $res{'status'} ) if $res{'status'};
+    $RT::Logger->warning( $res{'stderr'} ) if $res{'stderr'};
+    $RT::Logger->error( $res{'logger'} ) if $res{'logger'} && $?;
+    if ( $err || $res{'exit_code'} ) {
+        $res{'message'} = $err? $err : "gpg exitted with error code ". ($res{'exit_code'} >> 8);
+        return %res;
+    }
+
+    $entity->bodyhandle( new MIME::Body::File $tmp_fn );
+    $entity->{'__store_tmp_handle_to_avoid_early_cleanup'} = $tmp_fh;
+
+    return %res;
+}
+
+sub _SignEncryptAttachmentInline {
+    my %args = (
+        Entity => undef,
+
+        Sign => 1,
+        Signer => undef,
+        Passphrase => undef,
+
+        Encrypt => 1,
+        Recipients => undef,
+
+        @_
+    );
+    return unless $args{'Sign'} || $args{'Encrypt'};
+
+    my $gnupg = new GnuPG::Interface;
+    my %opt = RT->Config->Get('GnuPGOptions');
+
+    # handling passphrase in GnupGOptions
+    $args{'Passphrase'} = delete $opt{'passphrase'}
+        if !defined($args{'Passphrase'});
+
+    $opt{'digest-algo'} ||= 'SHA1';
+    $opt{'default_key'} = $args{'Signer'}
+        if $args{'Sign'} && $args{'Signer'};
+    $gnupg->options->hash_init(
+        _PrepareGnuPGOptions( %opt ),
+        armor => 1,
+        meta_interactive => 0,
+    );
+
+    if ( $args{'Sign'} && !defined $args{'Passphrase'} ) {
+        $args{'Passphrase'} = GetPassphrase( Address => $args{'Signer'} );
+    }
+
+    my $entity = $args{'Entity'};
+    if ( $args{'Encrypt'} ) {
+        $gnupg->options->push_recipients( $_ ) foreach
+            map UseKeyForEncryption($_) || $_,
+            @{ $args{'Recipients'} || [] };
+    }
+
+    my %res;
+
+    my ($tmp_fh, $tmp_fn) = File::Temp::tempfile( UNLINK => 1 );
+    binmode $tmp_fh, ':raw';
+
+    my ($handles, $handle_list) = _make_gpg_handles(stdout => $tmp_fh);
+    my %handle = %$handle_list;
+    $handles->options( 'stdout'  )->{'direct'} = 1;
+    $gnupg->passphrase( $args{'Passphrase'} ) if $args{'Sign'};
+
+    eval {
+        local $SIG{'CHLD'} = 'DEFAULT';
+        my $method = $args{'Sign'} && $args{'Encrypt'}
+            ? 'sign_and_encrypt'
+            : ($args{'Sign'}? 'detach_sign': 'encrypt');
+        my $pid = safe_run_child { $gnupg->$method( handles => $handles ) };
+        {
+            local $SIG{'PIPE'} = 'IGNORE';
+            $entity->bodyhandle->print( $handle{'stdin'} );
+            close $handle{'stdin'};
+        }
+        waitpid $pid, 0;
+    };
+    $res{'exit_code'} = $?;
+    my $err = $@;
+
+    foreach ( qw(stderr logger status) ) {
+        $res{$_} = do { local $/; readline $handle{$_} };
+        delete $res{$_} unless $res{$_} && $res{$_} =~ /\S/s;
+        close $handle{$_};
+    }
+    $RT::Logger->debug( $res{'status'} ) if $res{'status'};
+    $RT::Logger->warning( $res{'stderr'} ) if $res{'stderr'};
+    $RT::Logger->error( $res{'logger'} ) if $res{'logger'} && $?;
+    if ( $err || $res{'exit_code'} ) {
+        $res{'message'} = $err? $err : "gpg exitted with error code ". ($res{'exit_code'} >> 8);
+        return %res;
+    }
+
+    my $filename = $entity->head->recommended_filename || 'no_name';
+    if ( $args{'Sign'} && !$args{'Encrypt'} ) {
+        $entity->make_multipart;
+        $entity->attach(
+            Type     => 'application/octet-stream',
+            Path     => $tmp_fn,
+            Filename => "$filename.sig",
+            Disposition => 'attachment',
+        );
+    } else {
+        $entity->bodyhandle( new MIME::Body::File $tmp_fn );
+        $entity->effective_type('application/octet-stream');
+        $entity->head->mime_attr( $_ => "$filename.pgp" )
+            foreach (qw(Content-Type.name Content-Disposition.filename));
+
+    }
+    $entity->{'__store_tmp_handle_to_avoid_early_cleanup'} = $tmp_fh;
+
+    return %res;
+}
+
+sub SignEncryptContent {
+    my %args = (
+        Content => undef,
+
+        Sign => 1,
+        Signer => undef,
+        Passphrase => undef,
+
+        Encrypt => 1,
+        Recipients => undef,
+
+        @_
+    );
+    return unless $args{'Sign'} || $args{'Encrypt'};
+
+    my $gnupg = new GnuPG::Interface;
+    my %opt = RT->Config->Get('GnuPGOptions');
+
+    # handling passphrase in GnupGOptions
+    $args{'Passphrase'} = delete $opt{'passphrase'}
+        if !defined($args{'Passphrase'});
+
+    $opt{'digest-algo'} ||= 'SHA1';
+    $opt{'default_key'} = $args{'Signer'}
+        if $args{'Sign'} && $args{'Signer'};
+    $gnupg->options->hash_init(
+        _PrepareGnuPGOptions( %opt ),
+        armor => 1,
+        meta_interactive => 0,
+    );
+
+    if ( $args{'Sign'} && !defined $args{'Passphrase'} ) {
+        $args{'Passphrase'} = GetPassphrase( Address => $args{'Signer'} );
+    }
+
+    if ( $args{'Encrypt'} ) {
+        $gnupg->options->push_recipients( $_ ) foreach 
+            map UseKeyForEncryption($_) || $_,
+            @{ $args{'Recipients'} || [] };
+    }
+
+    my %res;
+
+    my ($tmp_fh, $tmp_fn) = File::Temp::tempfile( UNLINK => 1 );
+    binmode $tmp_fh, ':raw';
+
+    my ($handles, $handle_list) = _make_gpg_handles(stdout => $tmp_fh);
+    my %handle = %$handle_list;
+    $handles->options( 'stdout'  )->{'direct'} = 1;
+    $gnupg->passphrase( $args{'Passphrase'} ) if $args{'Sign'};
+
+    eval {
+        local $SIG{'CHLD'} = 'DEFAULT';
+        my $method = $args{'Sign'} && $args{'Encrypt'}
+            ? 'sign_and_encrypt'
+            : ($args{'Sign'}? 'clearsign': 'encrypt');
+        my $pid = safe_run_child { $gnupg->$method( handles => $handles ) };
+        {
+            local $SIG{'PIPE'} = 'IGNORE';
+            $handle{'stdin'}->print( ${ $args{'Content'} } );
+            close $handle{'stdin'};
+        }
+        waitpid $pid, 0;
+    };
+    $res{'exit_code'} = $?;
+    my $err = $@;
+
+    foreach ( qw(stderr logger status) ) {
+        $res{$_} = do { local $/; readline $handle{$_} };
+        delete $res{$_} unless $res{$_} && $res{$_} =~ /\S/s;
+        close $handle{$_};
+    }
+    $RT::Logger->debug( $res{'status'} ) if $res{'status'};
+    $RT::Logger->warning( $res{'stderr'} ) if $res{'stderr'};
+    $RT::Logger->error( $res{'logger'} ) if $res{'logger'} && $?;
+    if ( $err || $res{'exit_code'} ) {
+        $res{'message'} = $err? $err : "gpg exitted with error code ". ($res{'exit_code'} >> 8);
+        return %res;
+    }
+
+    ${ $args{'Content'} } = '';
+    seek $tmp_fh, 0, 0;
+    while (1) {
+        my $status = read $tmp_fh, my $buf, 4*1024;
+        unless ( defined $status ) {
+            $RT::Logger->crit( "couldn't read message: $!" );
+        } elsif ( !$status ) {
+            last;
+        }
+        ${ $args{'Content'} } .= $buf;
+    }
+
+    return %res;
+}
+
+sub FindProtectedParts {
+    my %args = ( Entity => undef, CheckBody => 1, @_ );
+    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;
+        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" );
+            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 ();
+        }
+
+        if ( $type eq 'multipart/encrypted' ) {
+            unless ( $protocol eq 'application/pgp-encrypted' ) {
+                $RT::Logger->info( "Skipping protocol '$protocol', only 'application/pgp-encrypted' is supported" );
+                return ();
+            }
+            $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 ();
+            }
+            $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);
+
+        # we can not associate a signature within an attachment
+        # without file names
+        my $fname = $part->head->recommended_filename;
+        next unless $fname;
+
+        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;
+        }
+    }
+
+    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)?$/;
+
+        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);
+
+        $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,
+        };
+    }
+
+    # 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 ."'");
+        push @res, {
+            Type      => 'encrypted',
+            Format    => 'Attachment',
+            Top     => $entity,
+            Data    => $part,
+        };
+    }
+
+    push @res, FindProtectedParts( Entity => $_ )
+        foreach grep !$skip{"$_"}, $entity->parts;
+
+    return @res;
+}
+
+=head2 VerifyDecrypt Entity => undef, [ Detach => 1, Passphrase => undef, SetStatus => 1 ]
+
+=cut
+
+sub VerifyDecrypt {
+    my %args = ( Entity => undef, Detach => 1, SetStatus => 1, @_ );
+    my @protected = FindProtectedParts( Entity => $args{'Entity'} );
+    my @res;
+    # XXX: detaching may brake nested signatures
+    foreach my $item( grep $_->{'Type'} eq 'signed', @protected ) {
+        if ( $item->{'Format'} eq 'RFC3156' ) {
+            push @res, { VerifyRFC3156( %$item, SetStatus => $args{'SetStatus'} ) };
+            if ( $args{'Detach'} ) {
+                $item->{'Top'}->parts( [ $item->{'Data'} ] );
+                $item->{'Top'}->make_singlepart;
+            }
+            $item->{'Top'}->head->set( 'X-RT-GnuPG-Status' => $res[-1]->{'status'} )
+                if $args{'SetStatus'};
+        } elsif ( $item->{'Format'} eq 'Inline' ) {
+            push @res, { VerifyInline( %$item ) };
+            $item->{'Data'}->head->set( 'X-RT-GnuPG-Status' => $res[-1]->{'status'} )
+                if $args{'SetStatus'};
+        } elsif ( $item->{'Format'} eq 'Attachment' ) {
+            push @res, { VerifyAttachment( %$item ) };
+            if ( $args{'Detach'} ) {
+                $item->{'Top'}->parts( [ grep "$_" ne $item->{'Signature'}, $item->{'Top'}->parts ] );
+                $item->{'Top'}->make_singlepart;
+            }
+            $item->{'Data'}->head->set( 'X-RT-GnuPG-Status' => $res[-1]->{'status'} )
+                if $args{'SetStatus'};
+        }
+    }
+    foreach my $item( grep $_->{'Type'} eq 'encrypted', @protected ) {
+        if ( $item->{'Format'} eq 'RFC3156' ) {
+            push @res, { DecryptRFC3156( %$item ) };
+            $item->{'Top'}->head->set( 'X-RT-GnuPG-Status' => $res[-1]->{'status'} )
+                if $args{'SetStatus'};
+        } elsif ( $item->{'Format'} eq 'Inline' ) {
+            push @res, { DecryptInline( %$item ) };
+            $item->{'Data'}->head->set( 'X-RT-GnuPG-Status' => $res[-1]->{'status'} )
+                if $args{'SetStatus'};
+        } elsif ( $item->{'Format'} eq 'Attachment' ) {
+            push @res, { DecryptAttachment( %$item ) };
+            $item->{'Data'}->head->set( 'X-RT-GnuPG-Status' => $res[-1]->{'status'} )
+                if $args{'SetStatus'};
+        }
+    }
+    return @res;
+}
+
+sub VerifyInline { return DecryptInline( @_ ) }
+
+sub VerifyAttachment {
+    my %args = ( Data => undef, Signature => undef, Top => undef, @_ );
+
+    my $gnupg = new GnuPG::Interface;
+    my %opt = RT->Config->Get('GnuPGOptions');
+    $opt{'digest-algo'} ||= 'SHA1';
+    $gnupg->options->hash_init(
+        _PrepareGnuPGOptions( %opt ),
+        meta_interactive => 0,
+    );
+
+    foreach ( $args{'Data'}, $args{'Signature'} ) {
+        next unless $_->bodyhandle->is_encoded;
+
+        require RT::EmailParser;
+        RT::EmailParser->_DecodeBody($_);
+    }
+
+    my ($tmp_fh, $tmp_fn) = File::Temp::tempfile( UNLINK => 1 );
+    binmode $tmp_fh, ':raw';
+    $args{'Data'}->bodyhandle->print( $tmp_fh );
+    $tmp_fh->flush;
+
+    my ($handles, $handle_list) = _make_gpg_handles();
+    my %handle = %$handle_list;
+
+    my %res;
+    eval {
+        local $SIG{'CHLD'} = 'DEFAULT';
+        my $pid = safe_run_child { $gnupg->verify(
+            handles => $handles, command_args => [ '-', $tmp_fn ]
+        ) };
+        {
+            local $SIG{'PIPE'} = 'IGNORE';
+            $args{'Signature'}->bodyhandle->print( $handle{'stdin'} );
+            close $handle{'stdin'};
+        }
+        waitpid $pid, 0;
+    };
+    $res{'exit_code'} = $?;
+    foreach ( qw(stderr logger status) ) {
+        $res{$_} = do { local $/; readline $handle{$_} };
+        delete $res{$_} unless $res{$_} && $res{$_} =~ /\S/s;
+        close $handle{$_};
+    }
+    $RT::Logger->debug( $res{'status'} ) if $res{'status'};
+    $RT::Logger->warning( $res{'stderr'} ) if $res{'stderr'};
+    $RT::Logger->error( $res{'logger'} ) if $res{'logger'} && $?;
+    if ( $@ || $? ) {
+        $res{'message'} = $@? $@: "gpg exitted with error code ". ($? >> 8);
+    }
+    return %res;
+}
+
+sub VerifyRFC3156 {
+    my %args = ( Data => undef, Signature => undef, Top => undef, @_ );
+
+    my $gnupg = new GnuPG::Interface;
+    my %opt = RT->Config->Get('GnuPGOptions');
+    $opt{'digest-algo'} ||= 'SHA1';
+    $gnupg->options->hash_init(
+        _PrepareGnuPGOptions( %opt ),
+        meta_interactive => 0,
+    );
+
+    my ($tmp_fh, $tmp_fn) = File::Temp::tempfile( UNLINK => 1 );
+    binmode $tmp_fh, ':raw:eol(CRLF?)';
+    $args{'Data'}->print( $tmp_fh );
+    $tmp_fh->flush;
+
+    my ($handles, $handle_list) = _make_gpg_handles();
+    my %handle = %$handle_list;
+
+    my %res;
+    eval {
+        local $SIG{'CHLD'} = 'DEFAULT';
+        my $pid = safe_run_child { $gnupg->verify(
+            handles => $handles, command_args => [ '-', $tmp_fn ]
+        ) };
+        {
+            local $SIG{'PIPE'} = 'IGNORE';
+            $args{'Signature'}->bodyhandle->print( $handle{'stdin'} );
+            close $handle{'stdin'};
+        }
+        waitpid $pid, 0;
+    };
+    $res{'exit_code'} = $?;
+    foreach ( qw(stderr logger status) ) {
+        $res{$_} = do { local $/; readline $handle{$_} };
+        delete $res{$_} unless $res{$_} && $res{$_} =~ /\S/s;
+        close $handle{$_};
+    }
+    $RT::Logger->debug( $res{'status'} ) if $res{'status'};
+    $RT::Logger->warning( $res{'stderr'} ) if $res{'stderr'};
+    $RT::Logger->error( $res{'logger'} ) if $res{'logger'} && $?;
+    if ( $@ || $? ) {
+        $res{'message'} = $@? $@: "gpg exitted with error code ". ($? >> 8);
+    }
+    return %res;
+}
+
+sub DecryptRFC3156 {
+    my %args = (
+        Data => undef,
+        Info => undef,
+        Top => undef,
+        Passphrase => undef,
+        @_
+    );
+
+    my $gnupg = new GnuPG::Interface;
+    my %opt = RT->Config->Get('GnuPGOptions');
+
+    # handling passphrase in GnupGOptions
+    $args{'Passphrase'} = delete $opt{'passphrase'}
+        if !defined($args{'Passphrase'});
+
+    $opt{'digest-algo'} ||= 'SHA1';
+    $gnupg->options->hash_init(
+        _PrepareGnuPGOptions( %opt ),
+        meta_interactive => 0,
+    );
+
+    if ( $args{'Data'}->bodyhandle->is_encoded ) {
+        require RT::EmailParser;
+        RT::EmailParser->_DecodeBody($args{'Data'});
+    }
+
+    $args{'Passphrase'} = GetPassphrase()
+        unless defined $args{'Passphrase'};
+
+    my ($tmp_fh, $tmp_fn) = File::Temp::tempfile( UNLINK => 1 );
+    binmode $tmp_fh, ':raw';
+
+    my ($handles, $handle_list) = _make_gpg_handles(stdout => $tmp_fh);
+    my %handle = %$handle_list;
+    $handles->options( 'stdout' )->{'direct'} = 1;
+
+    my %res;
+    eval {
+        local $SIG{'CHLD'} = 'DEFAULT';
+        $gnupg->passphrase( $args{'Passphrase'} );
+        my $pid = safe_run_child { $gnupg->decrypt( handles => $handles ) };
+        {
+            local $SIG{'PIPE'} = 'IGNORE';
+            $args{'Data'}->bodyhandle->print( $handle{'stdin'} );
+            close $handle{'stdin'}
+        }
+
+        waitpid $pid, 0;
+    };
+    $res{'exit_code'} = $?;
+    foreach ( qw(stderr logger status) ) {
+        $res{$_} = do { local $/; readline $handle{$_} };
+        delete $res{$_} unless $res{$_} && $res{$_} =~ /\S/s;
+        close $handle{$_};
+    }
+    $RT::Logger->debug( $res{'status'} ) if $res{'status'};
+    $RT::Logger->warning( $res{'stderr'} ) if $res{'stderr'};
+    $RT::Logger->error( $res{'logger'} ) if $res{'logger'} && $?;
+
+    # if the decryption is fine but the signature is bad, then without this
+    # status check we lose the decrypted text
+    # XXX: add argument to the function to control this check
+    if ( $res{'status'} !~ /DECRYPTION_OKAY/ ) {
+        if ( $@ || $? ) {
+            $res{'message'} = $@? $@: "gpg exitted with error code ". ($? >> 8);
+            return %res;
+        }
+    }
+
+    seek $tmp_fh, 0, 0;
+    my $parser = new RT::EmailParser;
+    my $decrypted = $parser->ParseMIMEEntityFromFileHandle( $tmp_fh, 0 );
+    $decrypted->{'__store_link_to_object_to_avoid_early_cleanup'} = $parser;
+    $args{'Top'}->parts( [] );
+    $args{'Top'}->add_part( $decrypted );
+    $args{'Top'}->make_singlepart;
+    return %res;
+}
+
+sub DecryptInline {
+    my %args = (
+        Data => undef,
+        Passphrase => undef,
+        @_
+    );
+
+    my $gnupg = new GnuPG::Interface;
+    my %opt = RT->Config->Get('GnuPGOptions');
+
+    # handling passphrase in GnuPGOptions
+    $args{'Passphrase'} = delete $opt{'passphrase'}
+        if !defined($args{'Passphrase'});
+
+    $opt{'digest-algo'} ||= 'SHA1';
+    $gnupg->options->hash_init(
+        _PrepareGnuPGOptions( %opt ),
+        meta_interactive => 0,
+    );
+
+    if ( $args{'Data'}->bodyhandle->is_encoded ) {
+        require RT::EmailParser;
+        RT::EmailParser->_DecodeBody($args{'Data'});
+    }
+
+    $args{'Passphrase'} = GetPassphrase()
+        unless defined $args{'Passphrase'};
+
+    my ($tmp_fh, $tmp_fn) = File::Temp::tempfile( UNLINK => 1 );
+    binmode $tmp_fh, ':raw';
+
+    my $io = $args{'Data'}->open('r');
+    unless ( $io ) {
+        die "Entity has no body, never should happen";
+    }
+
+    my ($had_literal, $in_block) = ('', 0);
+    my ($block_fh, $block_fn) = File::Temp::tempfile( UNLINK => 1 );
+    binmode $block_fh, ':raw';
+
+    my %res;
+    while ( defined(my $str = $io->getline) ) {
+        if ( $in_block && $str =~ /^-----END PGP (?:MESSAGE|SIGNATURE)-----/ ) {
+            print $block_fh $str;
+            $in_block--;
+            next if $in_block > 0;
+
+            seek $block_fh, 0, 0;
+
+            my ($res_fh, $res_fn);
+            ($res_fh, $res_fn, %res) = _DecryptInlineBlock(
+                %args,
+                GnuPG => $gnupg,
+                BlockHandle => $block_fh,
+            );
+            return %res unless $res_fh;
+
+            print $tmp_fh "-----BEGIN OF PGP PROTECTED PART-----\n" if $had_literal;
+            while (my $buf = <$res_fh> ) {
+                print $tmp_fh $buf;
+            }
+            print $tmp_fh "-----END OF PART-----\n" if $had_literal;
+
+            ($block_fh, $block_fn) = File::Temp::tempfile( UNLINK => 1 );
+            binmode $block_fh, ':raw';
+            $in_block = 0;
+        }
+        elsif ( $str =~ /^-----BEGIN PGP (SIGNED )?MESSAGE-----/ ) {
+            $in_block++;
+            print $block_fh $str;
+        }
+        elsif ( $in_block ) {
+            print $block_fh $str;
+        }
+        else {
+            print $tmp_fh $str;
+            $had_literal = 1 if /\S/s;
+        }
+    }
+    $io->close;
+
+    seek $tmp_fh, 0, 0;
+    $args{'Data'}->bodyhandle( new MIME::Body::File $tmp_fn );
+    $args{'Data'}->{'__store_tmp_handle_to_avoid_early_cleanup'} = $tmp_fh;
+    return %res;
+}
+
+sub _DecryptInlineBlock {
+    my %args = (
+        GnuPG => undef,
+        BlockHandle => undef,
+        Passphrase => undef,
+        @_
+    );
+    my $gnupg = $args{'GnuPG'};
+
+    my ($tmp_fh, $tmp_fn) = File::Temp::tempfile( UNLINK => 1 );
+    binmode $tmp_fh, ':raw';
+
+    my ($handles, $handle_list) = _make_gpg_handles(
+            stdin => $args{'BlockHandle'}, 
+            stdout => $tmp_fh);
+    my %handle = %$handle_list;
+    $handles->options( 'stdout' )->{'direct'} = 1;
+    $handles->options( 'stdin' )->{'direct'} = 1;
+
+    my %res;
+    eval {
+        local $SIG{'CHLD'} = 'DEFAULT';
+        $gnupg->passphrase( $args{'Passphrase'} );
+        my $pid = safe_run_child { $gnupg->decrypt( handles => $handles ) };
+        waitpid $pid, 0;
+    };
+    $res{'exit_code'} = $?;
+    foreach ( qw(stderr logger status) ) {
+        $res{$_} = do { local $/; readline $handle{$_} };
+        delete $res{$_} unless $res{$_} && $res{$_} =~ /\S/s;
+        close $handle{$_};
+    }
+    $RT::Logger->debug( $res{'status'} ) if $res{'status'};
+    $RT::Logger->warning( $res{'stderr'} ) if $res{'stderr'};
+    $RT::Logger->error( $res{'logger'} ) if $res{'logger'} && $?;
+
+    # if the decryption is fine but the signature is bad, then without this
+    # status check we lose the decrypted text
+    # XXX: add argument to the function to control this check
+    if ( $res{'status'} !~ /DECRYPTION_OKAY/ ) {
+        if ( $@ || $? ) {
+            $res{'message'} = $@? $@: "gpg exitted with error code ". ($? >> 8);
+            return (undef, undef, %res);
+        }
+    }
+
+    seek $tmp_fh, 0, 0;
+    return ($tmp_fh, $tmp_fn, %res);
+}
+
+sub DecryptAttachment {
+    my %args = (
+        Top  => undef,
+        Data => undef,
+        Passphrase => undef,
+        @_
+    );
+
+    my $gnupg = new GnuPG::Interface;
+    my %opt = RT->Config->Get('GnuPGOptions');
+
+    # handling passphrase in GnuPGOptions
+    $args{'Passphrase'} = delete $opt{'passphrase'}
+        if !defined($args{'Passphrase'});
+
+    $opt{'digest-algo'} ||= 'SHA1';
+    $gnupg->options->hash_init(
+        _PrepareGnuPGOptions( %opt ),
+        meta_interactive => 0,
+    );
+
+    if ( $args{'Data'}->bodyhandle->is_encoded ) {
+        require RT::EmailParser;
+        RT::EmailParser->_DecodeBody($args{'Data'});
+    }
+
+    $args{'Passphrase'} = GetPassphrase()
+        unless defined $args{'Passphrase'};
+
+    my ($tmp_fh, $tmp_fn) = File::Temp::tempfile( UNLINK => 1 );
+    binmode $tmp_fh, ':raw';
+    $args{'Data'}->bodyhandle->print( $tmp_fh );
+    seek $tmp_fh, 0, 0;
+
+    my ($res_fh, $res_fn, %res) = _DecryptInlineBlock(
+        %args,
+        GnuPG => $gnupg,
+        BlockHandle => $tmp_fh,
+    );
+    return %res unless $res_fh;
+
+    $args{'Data'}->bodyhandle( new MIME::Body::File $res_fn );
+    $args{'Data'}->{'__store_tmp_handle_to_avoid_early_cleanup'} = $res_fh;
+
+    my $filename = $args{'Data'}->head->recommended_filename;
+    $filename =~ s/\.pgp$//i;
+    $args{'Data'}->head->mime_attr( $_ => $filename )
+        foreach (qw(Content-Type.name Content-Disposition.filename));
+
+    return %res;
+}
+
+sub DecryptContent {
+    my %args = (
+        Content => undef,
+        Passphrase => undef,
+        @_
+    );
+
+    my $gnupg = new GnuPG::Interface;
+    my %opt = RT->Config->Get('GnuPGOptions');
+
+    # handling passphrase in GnupGOptions
+    $args{'Passphrase'} = delete $opt{'passphrase'}
+        if !defined($args{'Passphrase'});
+
+    $opt{'digest-algo'} ||= 'SHA1';
+    $gnupg->options->hash_init(
+        _PrepareGnuPGOptions( %opt ),
+        meta_interactive => 0,
+    );
+
+    $args{'Passphrase'} = GetPassphrase()
+        unless defined $args{'Passphrase'};
+
+    my ($tmp_fh, $tmp_fn) = File::Temp::tempfile( UNLINK => 1 );
+    binmode $tmp_fh, ':raw';
+
+    my ($handles, $handle_list) = _make_gpg_handles(
+            stdout => $tmp_fh);
+    my %handle = %$handle_list;
+    $handles->options( 'stdout' )->{'direct'} = 1;
+
+    my %res;
+    eval {
+        local $SIG{'CHLD'} = 'DEFAULT';
+        $gnupg->passphrase( $args{'Passphrase'} );
+        my $pid = safe_run_child { $gnupg->decrypt( handles => $handles ) };
+        {
+            local $SIG{'PIPE'} = 'IGNORE';
+            print { $handle{'stdin'} } ${ $args{'Content'} };
+            close $handle{'stdin'};
+        }
+
+        waitpid $pid, 0;
+    };
+    $res{'exit_code'} = $?;
+    foreach ( qw(stderr logger status) ) {
+        $res{$_} = do { local $/; readline $handle{$_} };
+        delete $res{$_} unless $res{$_} && $res{$_} =~ /\S/s;
+        close $handle{$_};
+    }
+    $RT::Logger->debug( $res{'status'} ) if $res{'status'};
+    $RT::Logger->warning( $res{'stderr'} ) if $res{'stderr'};
+    $RT::Logger->error( $res{'logger'} ) if $res{'logger'} && $?;
+
+    # if the decryption is fine but the signature is bad, then without this
+    # status check we lose the decrypted text
+    # XXX: add argument to the function to control this check
+    if ( $res{'status'} !~ /DECRYPTION_OKAY/ ) {
+        if ( $@ || $? ) {
+            $res{'message'} = $@? $@: "gpg exitted with error code ". ($? >> 8);
+            return %res;
+        }
+    }
+
+    ${ $args{'Content'} } = '';
+    seek $tmp_fh, 0, 0;
+    while (1) {
+        my $status = read $tmp_fh, my $buf, 4*1024;
+        unless ( defined $status ) {
+            $RT::Logger->crit( "couldn't read message: $!" );
+        } elsif ( !$status ) {
+            last;
+        }
+        ${ $args{'Content'} } .= $buf;
+    }
+
+    return %res;
+}
+
+=head2 GetPassphrase [ Address => undef ]
+
+Returns passphrase, called whenever it's required with Address as a named argument.
+
+=cut
+
+sub GetPassphrase {
+    my %args = ( Address => undef, @_ );
+    return 'test';
+}
+
+=head2 ParseStatus
+
+Takes a string containing output of gnupg status stream. Parses it and returns
+array of hashes. Each element of array is a hash ref and represents line or
+group of lines in the status message.
+
+All hashes have Operation, Status and Message elements.
+
+=over
+
+=item Operation
+
+Classification of operations gnupg performs. Now we have support
+for Sign, Encrypt, Decrypt, Verify, PassphraseCheck, RecipientsCheck and Data
+values.
+
+=item Status
+
+Informs about success. Value is 'DONE' on success, other values means that
+an operation failed, for example 'ERROR', 'BAD', 'MISSING' and may be other.
+
+=item Message
+
+User friendly message.
+
+=back
+
+This parser is based on information from GnuPG distribution, see also
+F<docs/design_docs/gnupg_details_on_output_formats> in the RT distribution.
+
+=cut
+
+my %REASON_CODE_TO_TEXT = (
+    NODATA => {
+        1 => "No armored data",
+        2 => "Expected a packet, but did not found one",
+        3 => "Invalid packet found",
+        4 => "Signature expected, but not found",
+    },
+    INV_RECP => {
+        0 => "No specific reason given",
+        1 => "Not Found",
+        2 => "Ambigious specification",
+        3 => "Wrong key usage",
+        4 => "Key revoked",
+        5 => "Key expired",
+        6 => "No CRL known",
+        7 => "CRL too old",
+        8 => "Policy mismatch",
+        9 => "Not a secret key",
+        10 => "Key not trusted",
+    },
+    ERRSIG => {
+        0 => 'not specified',
+        4 => 'unknown algorithm',
+        9 => 'missing public key',
+    },
+);
+
+sub ReasonCodeToText {
+    my $keyword = shift;
+    my $code = shift;
+    return $REASON_CODE_TO_TEXT{ $keyword }{ $code }
+        if exists $REASON_CODE_TO_TEXT{ $keyword }{ $code };
+    return 'unknown';
+}
+
+my %simple_keyword = (
+    NO_RECP => {
+        Operation => 'RecipientsCheck',
+        Status    => 'ERROR',
+        Message   => 'No recipients',
+    },
+    UNEXPECTED => {
+        Operation => 'Data',
+        Status    => 'ERROR',
+        Message   => 'Unexpected data has been encountered',
+    },
+    BADARMOR => {
+        Operation => 'Data',
+        Status    => 'ERROR',
+        Message   => 'The ASCII armor is corrupted',
+    },
+);
+
+# keywords we parse
+my %parse_keyword = map { $_ => 1 } qw(
+    USERID_HINT
+    SIG_CREATED GOODSIG BADSIG ERRSIG
+    END_ENCRYPTION
+    DECRYPTION_FAILED DECRYPTION_OKAY
+    BAD_PASSPHRASE GOOD_PASSPHRASE
+    NO_SECKEY NO_PUBKEY
+    NO_RECP INV_RECP NODATA UNEXPECTED
+);
+
+# keywords we ignore without any messages as we parse them using other
+# keywords as starting point or just ignore as they are useless for us
+my %ignore_keyword = map { $_ => 1 } qw(
+    NEED_PASSPHRASE MISSING_PASSPHRASE BEGIN_SIGNING PLAINTEXT PLAINTEXT_LENGTH
+    BEGIN_ENCRYPTION SIG_ID VALIDSIG
+    ENC_TO BEGIN_DECRYPTION END_DECRYPTION GOODMDC
+    TRUST_UNDEFINED TRUST_NEVER TRUST_MARGINAL TRUST_FULLY TRUST_ULTIMATE
+);
+
+sub ParseStatus {
+    my $status = shift;
+    return () unless $status;
+
+    my @status;
+    while ( $status =~ /\[GNUPG:\]\s*(.*?)(?=\[GNUPG:\]|\z)/igms ) {
+        push @status, $1; $status[-1] =~ s/\s+/ /g; $status[-1] =~ s/\s+$//;
+    }
+    $status = join "\n", @status;
+    study $status;
+
+    my @res;
+    my (%user_hint, $latest_user_main_key);
+    for ( my $i = 0; $i < @status; $i++ ) {
+        my $line = $status[$i];
+        my ($keyword, $args) = ($line =~ /^(\S+)\s*(.*)$/s);
+        if ( $simple_keyword{ $keyword } ) {
+            push @res, $simple_keyword{ $keyword };
+            $res[-1]->{'Keyword'} = $keyword;
+            next;
+        }
+        unless ( $parse_keyword{ $keyword } ) {
+            $RT::Logger->warning("Skipped $keyword") unless $ignore_keyword{ $keyword };
+            next;
+        }
+
+        if ( $keyword eq 'USERID_HINT' ) {
+            my %tmp = _ParseUserHint($status, $line);
+            $latest_user_main_key = $tmp{'MainKey'};
+            if ( $user_hint{ $tmp{'MainKey'} } ) {
+                while ( my ($k, $v) = each %tmp ) {
+                    $user_hint{ $tmp{'MainKey'} }->{$k} = $v;
+                }
+            } else {
+                $user_hint{ $tmp{'MainKey'} } = \%tmp;
+            }
+            next;
+        }
+        elsif ( $keyword eq 'BAD_PASSPHRASE' || $keyword eq 'GOOD_PASSPHRASE' ) {
+            my $key_id = $args;
+            my %res = (
+                Operation => 'PassphraseCheck',
+                Status    => $keyword eq 'BAD_PASSPHRASE'? 'BAD' : 'DONE',
+                Key       => $key_id,
+            );
+            $res{'Status'} = 'MISSING' if $status[ $i - 1 ] =~ /^MISSING_PASSPHRASE/;
+            foreach my $line ( reverse @status[ 0 .. $i-1 ] ) {
+                next unless $line =~ /^NEED_PASSPHRASE\s+(\S+)\s+(\S+)\s+(\S+)/;
+                next if $key_id && $2 ne $key_id;
+                @res{'MainKey', 'Key', 'KeyType'} = ($1, $2, $3);
+                last;
+            }
+            $res{'Message'} = ucfirst( lc( $res{'Status'} eq 'DONE'? 'GOOD': $res{'Status'} ) ) .' passphrase';
+            $res{'User'} = ( $user_hint{ $res{'MainKey'} } ||= {} ) if $res{'MainKey'};
+            if ( exists $res{'User'}->{'EmailAddress'} ) {
+                $res{'Message'} .= ' for '. $res{'User'}->{'EmailAddress'};
+            } else {
+                $res{'Message'} .= " for '0x$key_id'";
+            }
+            push @res, \%res;
+        }
+        elsif ( $keyword eq 'END_ENCRYPTION' ) {
+            my %res = (
+                Operation => 'Encrypt',
+                Status    => 'DONE',
+                Message   => 'Data has been encrypted',
+            );
+            foreach my $line ( reverse @status[ 0 .. $i-1 ] ) {
+                next unless $line =~ /^BEGIN_ENCRYPTION\s+(\S+)\s+(\S+)/;
+                @res{'MdcMethod', 'SymAlgo'} = ($1, $2);
+                last;
+            }
+            push @res, \%res;
+        }
+        elsif ( $keyword eq 'DECRYPTION_FAILED' || $keyword eq 'DECRYPTION_OKAY' ) {
+            my %res = ( Operation => 'Decrypt' );
+            @res{'Status', 'Message'} = 
+                $keyword eq 'DECRYPTION_FAILED'
+                ? ('ERROR', 'Decryption failed')
+                : ('DONE',  'Decryption process succeeded');
+
+            foreach my $line ( reverse @status[ 0 .. $i-1 ] ) {
+                next unless $line =~ /^ENC_TO\s+(\S+)\s+(\S+)\s+(\S+)/;
+                my ($key, $alg, $key_length) = ($1, $2, $3);
+
+                my %encrypted_to = (
+                    Message   => "The message is encrypted to '0x$key'",
+                    User      => ( $user_hint{ $key } ||= {} ),
+                    Key       => $key,
+                    KeyLength => $key_length,
+                    Algorithm => $alg,
+                );
+
+                push @{ $res{'EncryptedTo'} ||= [] }, \%encrypted_to;
+            }
+
+            push @res, \%res;
+        }
+        elsif ( $keyword eq 'NO_SECKEY' || $keyword eq 'NO_PUBKEY' ) {
+            my ($key) = split /\s+/, $args;
+            my $type = $keyword eq 'NO_SECKEY'? 'secret': 'public';
+            my %res = (
+                Operation => 'KeyCheck',
+                Status    => 'MISSING',
+                Message   => ucfirst( $type ) ." key '0x$key' is not available",
+                Key       => $key,
+                KeyType   => $type,
+            );
+            $res{'User'} = ( $user_hint{ $key } ||= {} );
+            $res{'User'}{ ucfirst( $type ). 'KeyMissing' } = 1;
+            push @res, \%res;
+        }
+        # GOODSIG, BADSIG, VALIDSIG, TRUST_*
+        elsif ( $keyword eq 'GOODSIG' ) {
+            my %res = (
+                Operation  => 'Verify',
+                Status     => 'DONE',
+                Message    => 'The signature is good',
+            );
+            @res{qw(Key UserString)} = split /\s+/, $args, 2;
+            $res{'Message'} .= ', signed by '. $res{'UserString'};
+
+            foreach my $line ( @status[ $i .. $#status ] ) {
+                next unless $line =~ /^TRUST_(\S+)/;
+                $res{'Trust'} = $1;
+                last;
+            }
+            $res{'Message'} .= ', trust level is '. lc( $res{'Trust'} || 'unknown');
+
+            foreach my $line ( @status[ $i .. $#status ] ) {
+                next unless $line =~ /^VALIDSIG\s+(.*)/;
+                @res{ qw(
+                    Fingerprint
+                    CreationDate
+                    Timestamp
+                    ExpireTimestamp
+                    Version
+                    Reserved
+                    PubkeyAlgo
+                    HashAlgo
+                    Class
+                    PKFingerprint
+                    Other
+                ) } = split /\s+/, $1, 10;
+                last;
+            }
+            push @res, \%res;
+        }
+        elsif ( $keyword eq 'BADSIG' ) {
+            my %res = (
+                Operation  => 'Verify',
+                Status     => 'BAD',
+                Message    => 'The signature has not been verified okay',
+            );
+            @res{qw(Key UserString)} = split /\s+/, $args, 2;
+            push @res, \%res;
+        }
+        elsif ( $keyword eq 'ERRSIG' ) {
+            my %res = (
+                Operation => 'Verify',
+                Status    => 'ERROR',
+                Message   => 'Not possible to check the signature',
+            );
+            @res{qw(Key PubkeyAlgo HashAlgo Class Timestamp ReasonCode Other)}
+                = split /\s+/, $args, 7;
+
+            $res{'Reason'} = ReasonCodeToText( $keyword, $res{'ReasonCode'} );
+            $res{'Message'} .= ", the reason is ". $res{'Reason'};
+
+            push @res, \%res;
+        }
+        elsif ( $keyword eq 'SIG_CREATED' ) {
+            # SIG_CREATED <type> <pubkey algo> <hash algo> <class> <timestamp> <key fpr>
+            my @props = split /\s+/, $args;
+            push @res, {
+                Operation      => 'Sign',
+                Status         => 'DONE',
+                Message        => "Signed message",
+                Type           => $props[0],
+                PubKeyAlgo     => $props[1],
+                HashKeyAlgo    => $props[2],
+                Class          => $props[3],
+                Timestamp      => $props[4],
+                KeyFingerprint => $props[5],
+                User           => $user_hint{ $latest_user_main_key },
+            };
+            $res[-1]->{Message} .= ' by '. $user_hint{ $latest_user_main_key }->{'EmailAddress'}
+                if $user_hint{ $latest_user_main_key };
+        }
+        elsif ( $keyword eq 'INV_RECP' ) {
+            my ($rcode, $recipient) = split /\s+/, $args, 2;
+            my $reason = ReasonCodeToText( $keyword, $rcode );
+            push @res, {
+                Operation  => 'RecipientsCheck',
+                Status     => 'ERROR',
+                Message    => "Recipient '$recipient' is unusable, the reason is '$reason'",
+                Recipient  => $recipient,
+                ReasonCode => $rcode,
+                Reason     => $reason,
+            };
+        }
+        elsif ( $keyword eq 'NODATA' ) {
+            my $rcode = (split /\s+/, $args)[0];
+            my $reason = ReasonCodeToText( $keyword, $rcode );
+            push @res, {
+                Operation  => 'Data',
+                Status     => 'ERROR',
+                Message    => "No data has been found. The reason is '$reason'",
+                ReasonCode => $rcode,
+                Reason     => $reason,
+            };
+        }
+        else {
+            $RT::Logger->warning("Keyword $keyword is unknown");
+            next;
+        }
+        $res[-1]{'Keyword'} = $keyword if @res && !$res[-1]{'Keyword'};
+    }
+    return @res;
+}
+
+sub _ParseUserHint {
+    my ($status, $hint) = (@_);
+    my ($main_key_id, $user_str) = ($hint =~ /^USERID_HINT\s+(\S+)\s+(.*)$/);
+    return () unless $main_key_id;
+    return (
+        MainKey      => $main_key_id,
+        String       => $user_str,
+        EmailAddress => (map $_->address, Email::Address->parse( $user_str ))[0],
+    );
+}
+
+sub _PrepareGnuPGOptions {
+    my %opt = @_;
+    my %res = map { lc $_ => $opt{ $_ } } grep $supported_opt{ lc $_ }, keys %opt;
+    $res{'extra_args'} ||= [];
+    foreach my $o ( grep !$supported_opt{ lc $_ }, keys %opt ) {
+        push @{ $res{'extra_args'} }, '--'. lc $o;
+        push @{ $res{'extra_args'} }, $opt{ $o }
+            if defined $opt{ $o };
+    }
+    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.
+
+B<Note> that keys for which trust level is not set are
+also listed.
+
+=cut
+
+sub GetKeysForEncryption {
+    my $key_id = shift;
+    my %res = GetKeysInfo( $key_id, 'public', @_ );
+    return %res if $res{'exit_code'};
+    return %res unless $res{'info'};
+
+    foreach my $key ( splice @{ $res{'info'} } ) {
+        # skip disabled keys
+        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,
+        # but leave keys with unknown trust level
+        next if $key->{'TrustLevel'} < 0;
+
+        push @{ $res{'info'} }, $key;
+    }
+    delete $res{'info'} unless @{ $res{'info'} };
+    return %res;
+}
+
+sub GetKeysForSigning {
+    my $key_id = shift;
+    return GetKeysInfo( $key_id, 'private', @_ );
+}
+
+sub CheckRecipients {
+    my @recipients = (@_);
+
+    my ($status, @issues) = (1, ());
+
+    my %seen;
+    foreach my $address ( grep !$seen{ lc $_ }++, map $_->address, @recipients ) {
+        my %res = GetKeysForEncryption( $address );
+        if ( $res{'info'} && @{ $res{'info'} } == 1 && $res{'info'}[0]{'TrustLevel'} > 0 ) {
+            # good, one suitable and trusted key 
+            next;
+        }
+        my $user = RT::User->new( $RT::SystemUser );
+        $user->LoadByEmail( $address );
+        # it's possible that we have no User record with the email
+        $user = undef unless $user->id;
+
+        if ( my $fpr = UseKeyForEncryption( $address ) ) {
+            if ( $res{'info'} && @{ $res{'info'} } ) {
+                next if
+                    grep lc $_->{'Fingerprint'} eq lc $fpr,
+                    grep $_->{'TrustLevel'} > 0,
+                    @{ $res{'info'} };
+            }
+
+            $status = 0;
+            my %issue = (
+                EmailAddress => $address,
+                $user? (User => $user) : (),
+                Keys => undef,
+            );
+            $issue{'Message'} = "Selected key either is not trusted or doesn't exist anymore."; #loc
+            push @issues, \%issue;
+            next;
+        }
+
+        my $prefered_key;
+        $prefered_key = $user->PreferredKey if $user;
+        #XXX: prefered key is not yet implemented...
+
+        # classify errors
+        $status = 0;
+        my %issue = (
+            EmailAddress => $address,
+            $user? (User => $user) : (),
+            Keys => undef,
+        );
+
+        unless ( $res{'info'} && @{ $res{'info'} } ) {
+            # no key
+            $issue{'Message'} = "There is no key suitable for encryption."; #loc
+        }
+        elsif ( @{ $res{'info'} } == 1 && !$res{'info'}[0]{'TrustLevel'} ) {
+            # trust is not set
+            $issue{'Message'} = "There is one suitable key, but trust level is not set."; #loc
+        }
+        else {
+            # multiple keys
+            $issue{'Message'} = "There are several keys suitable for encryption."; #loc
+        }
+        push @issues, \%issue;
+    }
+    return ($status, @issues);
+}
+
+sub GetPublicKeyInfo {
+    return GetKeyInfo( shift, 'public', @_ );
+}
+
+sub GetPrivateKeyInfo {
+    return GetKeyInfo( shift, 'private', @_ );
+}
+
+sub GetKeyInfo {
+    my %res = GetKeysInfo(@_);
+    $res{'info'} = $res{'info'}->[0];
+    return %res;
+}
+
+sub GetKeysInfo {
+    my $email = shift;
+    my $type = shift || 'public';
+    my $force = shift;
+
+    unless ( $email ) {
+        return (exit_code => 0) unless $force;
+    }
+
+    my $gnupg = new GnuPG::Interface;
+    my %opt = RT->Config->Get('GnuPGOptions');
+    $opt{'digest-algo'} ||= 'SHA1';
+    $opt{'with-colons'} = undef; # parseable format
+    $opt{'fingerprint'} = undef; # show fingerprint
+    $opt{'fixed-list-mode'} = undef; # don't merge uid with keys
+    $gnupg->options->hash_init(
+        _PrepareGnuPGOptions( %opt ),
+        armor => 1,
+        meta_interactive => 0,
+    );
+
+    my %res;
+
+    my ($handles, $handle_list) = _make_gpg_handles();
+    my %handle = %$handle_list;
+
+    eval {
+        local $SIG{'CHLD'} = 'DEFAULT';
+        my $method = $type eq 'private'? 'list_secret_keys': 'list_public_keys';
+        my $pid = safe_run_child { $gnupg->$method( handles => $handles, $email? (command_args => $email) : () ) };
+        close $handle{'stdin'};
+        waitpid $pid, 0;
+    };
+
+    my @info = readline $handle{'stdout'};
+    close $handle{'stdout'};
+
+    $res{'exit_code'} = $?;
+    foreach ( qw(stderr logger status) ) {
+        $res{$_} = do { local $/; readline $handle{$_} };
+        delete $res{$_} unless $res{$_} && $res{$_} =~ /\S/s;
+        close $handle{$_};
+    }
+    $RT::Logger->debug( $res{'status'} ) if $res{'status'};
+    $RT::Logger->warning( $res{'stderr'} ) if $res{'stderr'};
+    $RT::Logger->error( $res{'logger'} ) if $res{'logger'} && $?;
+    if ( $@ || $? ) {
+        $res{'message'} = $@? $@: "gpg exitted with error code ". ($? >> 8);
+        return %res;
+    }
+
+    @info = ParseKeysInfo( @info );
+    $res{'info'} = \@info;
+    return %res;
+}
+
+sub ParseKeysInfo {
+    my @lines = @_;
+
+    my %gpg_opt = RT->Config->Get('GnuPGOptions');
+
+    my @res = ();
+    foreach my $line( @lines ) {
+        chomp $line;
+        my $tag;
+        ($tag, $line) = split /:/, $line, 2;
+        if ( $tag eq 'pub' ) {
+            my %info;
+            @info{ qw(
+                TrustChar KeyLength Algorithm Key
+                Created Expire Empty OwnerTrustChar
+                Empty Empty Capabilities Other
+            ) } = split /:/, $line, 12;
+
+            # workaround gnupg's wierd behaviour, --list-keys command report calculated trust levels
+            # for any model except 'always', so you can change models and see changes, but not for 'always'
+            # we try to handle it in a simple way - we set ultimate trust for any key with trust
+            # level >= 0 if trust model is 'always'
+            my $always_trust;
+            $always_trust = 1 if exists $gpg_opt{'always-trust'};
+            $always_trust = 1 if exists $gpg_opt{'trust-model'} && $gpg_opt{'trust-model'} eq 'always';
+            @info{qw(Trust TrustTerse TrustLevel)} = 
+                _ConvertTrustChar( $info{'TrustChar'} );
+            if ( $always_trust && $info{'TrustLevel'} >= 0 ) {
+                @info{qw(Trust TrustTerse TrustLevel)} = 
+                    _ConvertTrustChar( 'u' );
+            }
+
+            @info{qw(OwnerTrust OwnerTrustTerse OwnerTrustLevel)} = 
+                _ConvertTrustChar( $info{'OwnerTrustChar'} );
+            $info{ $_ } = _ParseDate( $info{ $_ } )
+                foreach qw(Created Expire);
+            push @res, \%info;
+        }
+        elsif ( $tag eq 'sec' ) {
+            my %info;
+            @info{ qw(
+                Empty KeyLength Algorithm Key
+                Created Expire Empty OwnerTrustChar
+                Empty Empty Capabilities Other
+            ) } = split /:/, $line, 12;
+            @info{qw(OwnerTrust OwnerTrustTerse OwnerTrustLevel)} = 
+                _ConvertTrustChar( $info{'OwnerTrustChar'} );
+            $info{ $_ } = _ParseDate( $info{ $_ } )
+                foreach qw(Created Expire);
+            push @res, \%info;
+        }
+        elsif ( $tag eq 'uid' ) {
+            my %info;
+            @info{ qw(Trust Created Expire String) }
+                = (split /:/, $line)[0,4,5,8];
+            $info{ $_ } = _ParseDate( $info{ $_ } )
+                foreach qw(Created Expire);
+            push @{ $res[-1]{'User'} ||= [] }, \%info;
+        }
+        elsif ( $tag eq 'fpr' ) {
+            $res[-1]{'Fingerprint'} = (split /:/, $line, 10)[8];
+        }
+    }
+    return @res;
+}
+
+{
+    my %verbose = (
+        # deprecated
+        d   => [
+            "The key has been disabled", #loc
+            "key disabled", #loc
+            "-2"
+        ],
+
+        r   => [
+            "The key has been revoked", #loc
+            "key revoked", #loc
+            -3,
+        ],
+
+        e   => [ "The key has expired", #loc
+            "key expired", #loc
+            '-4',
+        ],
+
+        n   => [ "Don't trust this key at all", #loc
+            'none', #loc
+            -1,
+        ],
+
+        #gpupg docs says that '-' and 'q' may safely be treated as the same value
+        '-' => [
+            'Unknown (no trust value assigned)', #loc
+            'not set',
+            0,
+        ],
+        q   => [
+            'Unknown (no trust value assigned)', #loc
+            'not set',
+            0,
+        ],
+        o   => [
+            'Unknown (this value is new to the system)', #loc
+            'unknown',
+            0,
+        ],
+
+        m   => [
+            "There is marginal trust in this key", #loc
+            'marginal', #loc
+            1,
+        ],
+        f   => [
+            "The key is fully trusted", #loc
+            'full', #loc
+            2,
+        ],
+        u   => [
+            "The key is ultimately trusted", #loc
+            'ultimate', #loc
+            3,
+        ],
+    );
+
+    sub _ConvertTrustChar {
+        my $value = shift;
+        return @{ $verbose{'-'} } unless $value;
+        $value = substr $value, 0, 1;
+        return @{ $verbose{ $value } || $verbose{'o'} };
+    }
+}
+
+sub _ParseDate {
+    my $value = shift;
+    # never
+    return $value unless $value;
+
+    require RT::Date;
+    my $obj = RT::Date->new( $RT::SystemUser );
+    # unix time
+    if ( $value =~ /^\d+$/ ) {
+        $obj->Set( Value => $value );
+    } else {
+        $obj->Set( Format => 'unknown', Value => $value, Timezone => 'utc' );
+    }
+    return $obj;
+}
+
+sub DeleteKey {
+    my $key = shift;
+
+    my $gnupg = new GnuPG::Interface;
+    my %opt = RT->Config->Get('GnuPGOptions');
+    $gnupg->options->hash_init(
+        _PrepareGnuPGOptions( %opt ),
+        meta_interactive => 0,
+    );
+
+    my ($handles, $handle_list) = _make_gpg_handles();
+    my %handle = %$handle_list;
+
+    eval {
+        local $SIG{'CHLD'} = 'DEFAULT';
+        local @ENV{'LANG', 'LC_ALL'} = ('C', 'C');
+        my $pid = safe_run_child { $gnupg->wrap_call(
+            handles => $handles,
+            commands => ['--delete-secret-and-public-key'],
+            command_args => [$key],
+        ) };
+        close $handle{'stdin'};
+        while ( my $str = readline $handle{'status'} ) {
+            if ( $str =~ /^\[GNUPG:\]\s*GET_BOOL delete_key\..*/ ) {
+                print { $handle{'command'} } "y\n";
+            }
+        }
+        waitpid $pid, 0;
+    };
+    my $err = $@;
+    close $handle{'stdout'};
+
+    my %res;
+    $res{'exit_code'} = $?;
+    foreach ( qw(stderr logger status) ) {
+        $res{$_} = do { local $/; readline $handle{$_} };
+        delete $res{$_} unless $res{$_} && $res{$_} =~ /\S/s;
+        close $handle{$_};
+    }
+    $RT::Logger->debug( $res{'status'} ) if $res{'status'};
+    $RT::Logger->warning( $res{'stderr'} ) if $res{'stderr'};
+    $RT::Logger->error( $res{'logger'} ) if $res{'logger'} && $?;
+    if ( $err || $res{'exit_code'} ) {
+        $res{'message'} = $err? $err : "gpg exitted with error code ". ($res{'exit_code'} >> 8);
+    }
+    return %res;
+}
+
+sub ImportKey {
+    my $key = shift;
+
+    my $gnupg = new GnuPG::Interface;
+    my %opt = RT->Config->Get('GnuPGOptions');
+    $gnupg->options->hash_init(
+        _PrepareGnuPGOptions( %opt ),
+        meta_interactive => 0,
+    );
+
+    my ($handles, $handle_list) = _make_gpg_handles();
+    my %handle = %$handle_list;
+
+    eval {
+        local $SIG{'CHLD'} = 'DEFAULT';
+        local @ENV{'LANG', 'LC_ALL'} = ('C', 'C');
+        my $pid = safe_run_child { $gnupg->wrap_call(
+            handles => $handles,
+            commands => ['--import'],
+        ) };
+        print { $handle{'stdin'} } $key;
+        close $handle{'stdin'};
+        waitpid $pid, 0;
+    };
+    my $err = $@;
+    close $handle{'stdout'};
+
+    my %res;
+    $res{'exit_code'} = $?;
+    foreach ( qw(stderr logger status) ) {
+        $res{$_} = do { local $/; readline $handle{$_} };
+        delete $res{$_} unless $res{$_} && $res{$_} =~ /\S/s;
+        close $handle{$_};
+    }
+    $RT::Logger->debug( $res{'status'} ) if $res{'status'};
+    $RT::Logger->warning( $res{'stderr'} ) if $res{'stderr'};
+    $RT::Logger->error( $res{'logger'} ) if $res{'logger'} && $?;
+    if ( $err || $res{'exit_code'} ) {
+        $res{'message'} = $err? $err : "gpg exitted with error code ". ($res{'exit_code'} >> 8);
+    }
+    return %res;
+}
+
+=head2 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
+of the signer: either email address, key id or finger print.
+
+Returns a true value if all went well.
+
+=cut
+
+sub DrySign {
+    my $from = shift;
+
+    my $mime = MIME::Entity->build(
+        Type    => "text/plain",
+        From    => 'nobody@localhost',
+        To      => 'nobody@localhost',
+        Subject => "dry sign",
+        Data    => ['t'],
+    );
+
+    my %res = SignEncrypt(
+        Sign    => 1,
+        Encrypt => 0,
+        Entity  => $mime,
+        Signer  => $from,
+    );
+
+    return $res{exit_code} == 0;
+}
+
+1;
+
+=head2 Probe
+
+This routine returns true if RT's GnuPG support is configured and working 
+properly (and false otherwise).
+
+
+=cut
+
+
+sub Probe {
+    my $gnupg = new GnuPG::Interface;
+    my %opt = RT->Config->Get('GnuPGOptions');
+    $gnupg->options->hash_init(
+        _PrepareGnuPGOptions( %opt ),
+        armor => 1,
+        meta_interactive => 0,
+    );
+
+    my ($handles, $handle_list) = _make_gpg_handles();
+    my %handle = %$handle_list;
+
+    local $@;
+    eval {
+        local $SIG{'CHLD'} = 'DEFAULT';
+        my $pid = safe_run_child { $gnupg->wrap_call( commands => ['--version' ], handles => $handles ) };
+        close $handle{'stdin'};
+        waitpid $pid, 0;
+    };
+    if ( $@ ) {
+        $RT::Logger->debug(
+            "Probe for GPG failed."
+            ." Couldn't run `gpg --version`: ". $@
+        );
+        return 0;
+    }
+
+# on some systems gpg exits with code 2, but still 100% functional,
+# it's general error system error or incorrect command, command is correct,
+# but there is no way to get actuall error
+    if ( $? && ($? >> 8) != 2 ) {
+        $RT::Logger->debug(
+            "Probe for GPG failed."
+            ." Process exitted with code ". ($? >> 8)
+            . ($? & 127 ? (" as recieved signal ". ($? & 127)) : '')
+        );
+        return 0;
+    }
+    return 1;
+}
+
+
+sub _make_gpg_handles {
+    my %handle_map = (
+        stdin  => IO::Handle->new(),
+        stdout => IO::Handle->new(),
+        stderr => IO::Handle->new(),
+        logger => IO::Handle->new(),
+        status => IO::Handle->new(),
+        command => IO::Handle->new(),
+
+
+            @_);
+
+    my $handles = GnuPG::Handles->new(%handle_map);
+    return ($handles, \%handle_map);
+}
+
+eval "require RT::Crypt::GnuPG_Vendor";
+if ($@ && $@ !~ qr{^Can't locate RT/Crypt/GnuPG_Vendor.pm}) {
+    die $@;
+};
+
+eval "require RT::Crypt::GnuPG_Local";
+if ($@ && $@ !~ qr{^Can't locate RT/Crypt/GnuPG_Local.pm}) {
+    die $@;
+};
+
+# helper package to avoid using temp file
+package IO::Handle::CRLF;
+
+use base qw(IO::Handle);
+
+sub print {
+    my ($self, @args) = (@_);
+    s/\r*\n/\x0D\x0A/g foreach @args;
+    return $self->SUPER::print( @args );
+}
+
+1;