App-mailmake

 view release on metacpan or  search on metacpan

scripts/mailmake  view on Meta::CPAN

our $PROG_NAME = file( __FILE__ )->basename( '.pl' );

$SIG{INT} = $SIG{TERM} = \&_signal_handler;

our $out = stdout( binmode => 'utf-8', autoflush => 1 );
our $err = stderr( binmode => 'utf-8', autoflush => 1 );
@ARGV = map( Encode::decode_utf8( $_ ), @ARGV );

# NOTE: options dictionary
# Tokens use underscores; Getopt::Class automatically exposes them as dashes on the
# command line. Example: gpg_key_id -> --gpg-key-id
my $dict =
{
    # Envelope / headers
    from            => { type => 'string',  alias => [qw( f )], required => 1 },
    to              => { type => 'array',   alias => [qw( t )], required => 1 },
    cc              => { type => 'array' },
    bcc             => { type => 'array' },
    reply_to        => { type => 'string' },
    sender          => { type => 'string' },
    subject         => { type => 'string',  alias => [qw( s )] },

scripts/mailmake  view on Meta::CPAN


    # SMTP delivery
    smtp_host       => { type => 'string',  alias => [qw( host H )] },
    smtp_port       => { type => 'integer', alias => [qw( port P )] },
    smtp_user       => { type => 'string',  alias => [qw( user U )] },
    smtp_password   => { type => 'string',  alias => [qw( password )] },
    smtp_tls        => { type => 'boolean', default => 0 },  # SMTPS port 465
    smtp_starttls   => { type => 'boolean', default => 0 },  # STARTTLS
    smtp_timeout    => { type => 'integer', default => 30 },

    # OpenPGP (Mail::Make::GPG - requires gpg and IPC::Run)
    gpg_sign        => { type => 'boolean', default => 0 },
    gpg_encrypt     => { type => 'boolean', default => 0 },
    gpg_key_id      => { type => 'string' },
    gpg_passphrase  => { type => 'string' },
    gpg_recipients  => { type => 'array' },   # defaults to --to if omitted
    gpg_digest      => { type => 'string',  default => 'SHA256' },
    gpg_bin         => { type => 'string' },
    gpg_keyserver   => { type => 'string' },
    gpg_autofetch   => { type => 'boolean', default => 0 },

    # S/MIME (Mail::Make::SMIME - requires Crypt::SMIME)
    smime_sign          => { type => 'boolean', default => 0 },
    smime_encrypt       => { type => 'boolean', default => 0 },
    smime_cert          => { type => 'file' },    # signer cert (PEM)
    smime_key           => { type => 'file' },    # signer private key (PEM)
    smime_key_password  => { type => 'string' },
    smime_ca_cert       => { type => 'file' },    # CA cert (PEM)
    smime_recipient_cert => { type => 'file-array' },  # recipient cert(s) (PEM)

scripts/mailmake  view on Meta::CPAN

    $out->print( "Perl warning only: ", @_, "\n" ) if( $LOG_LEVEL >= 5 );
};

unless( $LOG_LEVEL )
{
    $LOG_LEVEL = 1 if( $VERBOSE );
    $LOG_LEVEL = ( 1 + $DEBUG ) if( $DEBUG );
}

# NOTE: Validate mutually exclusive crypto options
if( $opts->{gpg_sign} || $opts->{gpg_encrypt} )
{
    if( $opts->{smime_sign} || $opts->{smime_encrypt} )
    {
        push( @errors, "Cannot combine --gpg-* and --smime-* options." );
    }
}

if( @errors )
{
    my $error = join( "\n", map{ "\t* $_" } @errors );
    substr( $error, 0, 0, "\n\tThe following errors were found.\n" );
    unless( $opts->{quiet} )
    {
        $err->print( <<EOT );

scripts/mailmake  view on Meta::CPAN

    if( defined( $opts->{attach_inline} ) && @{$opts->{attach_inline}} )
    {
        for my $f ( @{$opts->{attach_inline}} )
        {
            _die( "Inline attachment file \"$f\" does not exist." ) unless( $f->exists );
            $mail->attach_inline( path => "$f" ) || _die( $mail->error );
        }
    }

    # Cryptographic operations
    if( $opts->{gpg_sign} || $opts->{gpg_encrypt} )
    {
        $mail = _apply_gpg( $mail ) || return(0);
    }
    elsif( $opts->{smime_sign} || $opts->{smime_encrypt} )
    {
        $mail = _apply_smime( $mail ) || return(0);
    }

    # Output or deliver
    if( $opts->{print} )
    {
        my $str = $mail->as_string ||
            _die( "Failed to serialise message: ", $mail->error );
        $out->print( $str );
        return(1);
    }

    return( _deliver( $mail ) );
}

# _apply_gpg( $mail ) → $mail (possibly a new object) or undef on error
sub _apply_gpg
{
    my $mail = shift( @_ );

    my %gpg_opts = (
        Digest  => $opts->{gpg_digest},
    );
    $gpg_opts{GpgBin}    = $opts->{gpg_bin}        if( defined( $opts->{gpg_bin} ) );
    $gpg_opts{KeyServer} = $opts->{gpg_keyserver}  if( defined( $opts->{gpg_keyserver} ) );
    $gpg_opts{AutoFetch} = 1                       if( $opts->{gpg_autofetch} );

    if( $opts->{gpg_sign} || ( $opts->{gpg_sign} && $opts->{gpg_encrypt} ) )
    {
        $gpg_opts{KeyId}      = $opts->{gpg_key_id}     if( defined( $opts->{gpg_key_id} ) );
        $gpg_opts{Passphrase} = $opts->{gpg_passphrase} if( defined( $opts->{gpg_passphrase} ) );
    }

    if( $opts->{gpg_encrypt} )
    {
        # Default recipients to --to if --gpg-recipients not specified
        my $rcpts = $opts->{gpg_recipients};
        if( !defined( $rcpts ) || !@{$rcpts} )
        {
            $rcpts = $opts->{to};
        }

        unless( defined( $rcpts ) && @{$rcpts} )
        {
            _die( "GPG encryption requires at least one recipient (--gpg-recipients or --to)." );
        }
        $gpg_opts{Recipients} = $rcpts;
    }

    my $result;
    if( $opts->{gpg_sign} && $opts->{gpg_encrypt} )
    {
        _message( 2, "Applying GPG sign+encrypt." );
        $result = $mail->gpg_sign_encrypt( %gpg_opts );
    }
    elsif( $opts->{gpg_encrypt} )
    {
        _message( 2, "Applying GPG encryption." );
        $result = $mail->gpg_encrypt( %gpg_opts );
    }
    else
    {
        _message( 2, "Applying GPG signature." );
        $result = $mail->gpg_sign( %gpg_opts );
    }

    unless( defined( $result ) )
    {
        _message( 1, "<red>GPG operation failed:</> ", $mail->error );
        return;
    }
    return( $result );
}

scripts/mailmake  view on Meta::CPAN

             --smtp-host mail.example.com --smtp-port 587 --smtp-starttls \
             --smtp-user alice@example.com --smtp-password secret

    # Print the raw RFC 2822 message instead of sending
    mailmake --from alice@example.com --to bob@example.com \
             --subject "Test" --plain "Test" --print

    # OpenPGP detached signature
    mailmake --from alice@example.com --to bob@example.com \
             --subject "Signed" --plain "Signed message." \
             --gpg-sign --gpg-key-id FINGERPRINT \
             --smtp-host mail.example.com

    # OpenPGP sign + encrypt
    mailmake --from alice@example.com --to bob@example.com \
             --subject "Secret" --plain "Encrypted message." \
             --gpg-sign --gpg-encrypt \
             --gpg-key-id FINGERPRINT --gpg-passphrase secret \
             --smtp-host mail.example.com

    # S/MIME signature
    mailmake --from alice@example.com --to bob@example.com \
             --subject "Signed" --plain "Signed message." \
             --smime-sign \
             --smime-cert /path/to/my.cert.pem \
             --smime-key  /path/to/my.key.pem \
             --smime-ca-cert /path/to/ca.crt \
             --smtp-host mail.example.com

scripts/mailmake  view on Meta::CPAN

=item B<--smtp-tls>

Use direct TLS from the start (SMTPS, typically port 465).

=item B<--smtp-timeout> SECONDS

Connection and command timeout. Default: 30.

=back

=head2 OpenPGP (requires C<gpg> / C<gpg2> and L<IPC::Run>)

OpenPGP options cannot be combined with S/MIME options.

=over 4

=item B<--gpg-sign>

Sign the message (RFC 3156 C<multipart/signed> with detached ASCII signature).

=item B<--gpg-encrypt>

Encrypt the message (RFC 3156 C<multipart/encrypted>).

C<--gpg-sign> and C<--gpg-encrypt> may be combined for sign-then-encrypt.

=item B<--gpg-key-id> FINGERPRINT

Signing key fingerprint or ID (required when C<--gpg-sign> is used).

=item B<--gpg-passphrase> PASSPHRASE

Passphrase to unlock the secret key. When omitted, C<gpg-agent> is expected to handle passphrase prompting.

=item B<--gpg-recipients> ADDRESS [ADDRESS ...]

Recipient key IDs or e-mail addresses for encryption. Defaults to C<--to> when not specified.

=item B<--gpg-digest> ALGORITHM

Hash algorithm for signatures. Default: C<SHA256>.

Valid values: C<SHA256>, C<SHA384>, C<SHA512>, C<SHA1>.

=item B<--gpg-bin> PATH

Full path to the C<gpg> executable. Defaults to searching C<gpg2> then C<gpg> in C<PATH>.

=item B<--gpg-keyserver> URL

Keyserver URL for auto-fetching recipient keys. Only consulted when C<--gpg-autofetch> is set. Example: C<keys.openpgp.org>

=item B<--gpg-autofetch>

Fetch missing recipient public keys from C<--gpg-keyserver> before encrypting.

=back

=head2 S/MIME (requires L<Crypt::SMIME>)

S/MIME options cannot be combined with OpenPGP options.

=over 4

=item B<--smime-sign>

t/010_app_mailmake.t  view on Meta::CPAN

subtest 'missing --from should fail' => sub
{
    my $cmd = qq{$^X -Ilib scripts/mailmake} .
              qq{ --to recipient\@example.com --plain "x" --print};
    diag( "Running $cmd" ) if( $DEBUG );
    my( $out, $err, $exit ) = _run( $cmd );
    $out .= $err;  # merge for error-checking tests
    isnt( $exit, 0, 'missing --from: non-zero exit' );
};

# NOTE: --gpg-sign and --smime-sign together should fail
subtest '--gpg-sign and --smime-sign together should fail' => sub
{
    my $cmd = $base_cmd .
              qq{ --plain "x" --gpg-sign --smime-sign --print};
    diag( "Running $cmd" ) if( $DEBUG );
    my( $out, $err, $exit ) = _run( $cmd );
    $out .= $err;  # merge for error-checking tests
    isnt( $exit, 0, 'conflicting gpg+smime: non-zero exit' );
};

done_testing();

__END__



( run in 0.852 second using v1.01-cache-2.11-cpan-e1769b4cff6 )