File Coverage

lib/Mail/Make/Stream/Base64.pm
Criterion Covered Total %
statement 80 94 85.1
branch 11 26 42.3
condition 3 9 33.3
subroutine 16 20 80.0
pod 6 6 100.0
total 116 155 74.8


line stmt bran cond sub pod time code
1             ##----------------------------------------------------------------------------
2             ## MIME Email Builder - ~/lib/Mail/Make/Stream/Base64.pm
3             ## Version v0.3.0
4             ## Copyright(c) 2026 DEGUEST Pte. Ltd.
5             ## Author: Jacques Deguest <jack@deguest.jp>
6             ## Created 2026/03/02
7             ## Modified 2026/03/05
8             ## All rights reserved
9             ##
10             ## This program is free software; you can redistribute it and/or modify it
11             ## under the same terms as Perl itself.
12             ##----------------------------------------------------------------------------
13             package Mail::Make::Stream::Base64;
14             BEGIN
15             {
16 8     8   1279 use strict;
  8         19  
  8         312  
17 8     8   38 use warnings;
  8         15  
  8         512  
18 8     8   77 warnings::register_categories( 'Mail::Make' );
19 8     8   534 use Mail::Make::Stream;
  8         15  
  8         63  
20 8     8   2640 use parent -norequire, qw( Mail::Make::Stream::Generic );
  8         18  
  8         98  
21 8     8   588 use vars qw( @EXPORT_OK $VERSION $EXCEPTION_CLASS $Base64Error );
  8         19  
  8         585  
22 8     8   49 use Exporter qw( import );
  8         14  
  8         319  
23 8     8   37 use Mail::Make::Exception;
  8         12  
  8         54  
24 8     8   2749 use MIME::Base64 ();
  8         882  
  8         319  
25             use constant
26             {
27             # Input chunk must be a multiple of 3 so base64 output lines align cleanly
28 8         1178 ENCODE_BUFFER_SIZE => 300,
29             DECODE_BUFFER_SIZE => ( 32 * 1024 ),
30 8     8   41 };
  8         16  
31 8         21 our @EXPORT_OK = qw( decode_b64 encode_b64 );
32 8         46 our $EXCEPTION_CLASS = 'Mail::Make::Exception';
33 8         234 our $VERSION = 'v0.3.0';
34             };
35              
36 8     8   55 use strict;
  8         13  
  8         238  
37 8     8   34 use warnings;
  8         15  
  8         7334  
38              
39             # decode( $from, $to [, %opts] )
40             # Decodes base64 data from $from and writes raw bytes to $to.
41             # Each of $from / $to may be: a filehandle, a scalar reference, or a file path.
42             sub decode
43             {
44 4     4 1 5697 my $self = shift( @_ );
45 4         5 my $from = shift( @_ );
46 4         6 my $to = shift( @_ );
47 4         10 my $opts = $self->_get_args_as_hash( @_ );
48 4         20 my( $from_fh, $reader ) = $self->_get_glob_from_arg( $from );
49 4         10 my( $to_fh, $writer ) = $self->_get_glob_from_arg( $to, write => 1 );
50 4 50 33     16 return( $self->pass_error ) if( !defined( $from_fh ) || !defined( $to_fh ) );
51 4         5 my( $n, $buff );
52              
53 4         11 while( $n = $reader->( $buff, DECODE_BUFFER_SIZE ) )
54             {
55 4         14 my $decoded = MIME::Base64::decode_base64( $buff );
56 4         6 my $rv = $writer->( $decoded );
57 4 50       10 return( $self->pass_error ) if( !defined( $rv ) );
58             }
59 4 50       7 return( $self->pass_error ) if( !defined( $n ) );
60 4         21 return( $self );
61             }
62              
63             sub decode_b64
64             {
65 1     1 1 845 my $s = __PACKAGE__->new;
66 1         6 my $rv = $s->decode( @_ );
67 1 50       2543 if( !defined( $rv ) )
68             {
69 0         0 $Base64Error = $s->error;
70 0         0 return( $s->pass_error );
71             }
72             else
73             {
74 1         2 undef( $Base64Error );
75 1         3 return( $rv );
76             }
77             }
78              
79             # encode( $from, $to [, %opts] )
80             # Encodes raw data from $from as RFC 2045 base64 and writes to $to.
81             # Options:
82             # eol => $str line ending after each 76-char line (default CRLF "\015\012")
83             sub encode
84             {
85 18     18 1 88 my $self = shift( @_ );
86 18         46 my $from = shift( @_ );
87 18         44 my $to = shift( @_ );
88 18         148 my $opts = $self->_get_args_as_hash( @_ );
89 18         229 my( $from_fh, $reader ) = $self->_get_glob_from_arg( $from );
90 18         85 my( $to_fh, $writer ) = $self->_get_glob_from_arg( $to, write => 1 );
91 18 50 33     222 return( $self->pass_error ) if( !defined( $from_fh ) || !defined( $to_fh ) );
92 18 50       122 my $eol = exists( $opts->{eol} ) ? $opts->{eol} : "\015\012";
93 18   33     291 my $has_eol = defined( $eol ) && length( $eol );
94 18         58 my( $n, $buff );
95              
96 18         185 while( $n = $reader->( $buff, ENCODE_BUFFER_SIZE ) )
97             {
98             # MIME::Base64::encode_base64 appends its own newline - strip it, then
99             # insert our configured eol every 76 characters.
100 21         151 my $encoded = MIME::Base64::encode_base64( $buff, '' );
101 21 50       75 if( $has_eol )
102             {
103 21         143 $encoded =~ s/(.{76})/$1$eol/g;
104             # Ensure a trailing eol if the final chunk did not land on a 76-char boundary
105 21 50       149 $encoded .= $eol unless( $encoded =~ /\015\012$/ );
106             }
107 21         86 my $rv = $writer->( $encoded );
108 21 50       109 return( $self->pass_error ) if( !defined( $rv ) );
109             }
110 18 50       57 return( $self->pass_error ) if( !defined( $n ) );
111 18         187 return( $self );
112             }
113              
114             sub encode_b64
115             {
116 1     1 1 5662 my $s = __PACKAGE__->new;
117 1         5 my $rv = $s->encode( @_ );
118 1 50       2486 if( !defined( $rv ) )
119             {
120 0         0 $Base64Error = $s->error;
121 0         0 return( $s->pass_error );
122             }
123             else
124             {
125 1         2 undef( $Base64Error );
126 1         3 return( $rv );
127             }
128             }
129              
130             sub is_decoder_installed
131             {
132 0     0 1   local $@;
133             eval
134 0           {
135 0     0     local $SIG{__DIE__} = sub{};
136 0           require MIME::Base64;
137             };
138 0 0         return( $@ ? 0 : 1 );
139             }
140              
141             sub is_encoder_installed
142             {
143 0     0 1   local $@;
144             eval
145 0           {
146 0     0     local $SIG{__DIE__} = sub{};
147 0           require MIME::Base64;
148             };
149 0 0         return( $@ ? 0 : 1 );
150             }
151              
152             # NOTE: sub FREEZE is inherited
153              
154             # NOTE: sub STORABLE_freeze is inherited
155              
156             # NOTE: sub STORABLE_thaw is inherited
157              
158             # NOTE: sub THAW is inherited
159              
160             1;
161             # NOTE: POD
162             __END__
163              
164             =encoding utf-8
165              
166             =head1 NAME
167              
168             Mail::Make::Stream::Base64 - Streaming Base64 Encoder/Decoder for Mail::Make
169              
170             =head1 SYNOPSIS
171              
172             use Mail::Make::Stream::Base64;
173              
174             my $s = Mail::Make::Stream::Base64->new ||
175             die( Mail::Make::Stream::Base64->error, "\n" );
176              
177             # File to file
178             $s->encode( '/path/to/logo.png' => '/tmp/logo.b64' ) ||
179             die( $s->error );
180              
181             # Scalar ref to scalar ref
182             my( $raw, $out ) = ( "Hello, world!" );
183             $s->encode( \$raw => \$out ) || die( $s->error );
184              
185             # Decode
186             my $decoded = '';
187             $s->decode( \$out => \$decoded ) || die( $s->error );
188              
189             # Exportable wrappers
190             use Mail::Make::Stream::Base64 qw( encode_b64 decode_b64 );
191             encode_b64( \$raw => \$out ) ||
192             die( $Mail::Make::Stream::Base64::Base64Error );
193              
194             =head1 VERSION
195              
196             v0.3.0
197              
198             =head1 DESCRIPTION
199              
200             RFC 2045 compliant Base64 encoder and decoder. Both L</encode> and L</decode> operate as stream pipelines: data is read from C<$from> in chunks and written to C<$to> without accumulating the full content in memory, making them safe for large attachments backed by L<Mail::Make::Body::File>.
201              
202             Each of C<$from> and C<$to> may be:
203              
204             =over 4
205              
206             =item * A native filehandle or IO object
207              
208             =item * A scalar reference (C<\$scalar>)
209              
210             =item * A plain string (file path)
211              
212             =back
213              
214             =head1 METHODS
215              
216             =head2 decode( $from, $to )
217              
218             Reads base64-encoded data from C<$from>, decodes it via L<MIME::Base64>, and writes the raw bytes to C<$to>. Returns C<$self> on success, C<undef> on error.
219              
220             =head2 encode( $from, $to [, %opts] )
221              
222             Reads raw bytes from C<$from> in 300-byte chunks, encodes them as RFC 2045 base64 folded at 76 characters per line, and writes the result to C<$to>.
223              
224             Returns C<$self> on success, C<undef> on error.
225              
226             Options:
227              
228             =over 4
229              
230             =item C<eol>
231              
232             Line ending appended after each 76-character line. Defaults to CRLF (C<"\015\012">). Pass C<undef> or C<""> to suppress line folding.
233              
234             =back
235              
236             =head1 CLASS FUNCTIONS
237              
238             The following functions are exportable on request:
239              
240             use Mail::Make::Stream::Base64 qw( encode_b64 decode_b64 );
241              
242             =head2 encode_b64( $from, $to [, %opts] )
243              
244             Convenience wrapper for L</encode>. Sets C<$Base64Error> and returns C<undef> on failure.
245              
246             =head2 decode_b64( $from, $to )
247              
248             Convenience wrapper for L</decode>. Sets C<$Base64Error> and returns C<undef> on failure.
249              
250             =head2 is_encoder_installed
251              
252             =head2 is_decoder_installed
253              
254             Return true if L<MIME::Base64> is available.
255              
256             =head1 AUTHOR
257              
258             Jacques Deguest E<lt>F<jack@deguest.jp>E<gt>
259              
260             =head1 SEE ALSO
261              
262             L<Mail::Make>, L<Mail::Make::Entity>, L<Mail::Make::Stream::QuotedPrint>, L<Mail::Make::Stream>, L<MIME::Base64>
263              
264             RFC 2045
265              
266             =head1 COPYRIGHT & LICENSE
267              
268             Copyright(c) 2026 DEGUEST Pte. Ltd.
269              
270             All rights reserved.
271              
272             This program is free software; you can redistribute it and/or modify it under the same terms as Perl itself.
273              
274             =cut