File Coverage

lib/Mail/Make/Stream/QuotedPrint.pm
Criterion Covered Total %
statement 85 108 78.7
branch 22 50 44.0
condition 11 27 40.7
subroutine 16 21 76.1
pod 7 7 100.0
total 141 213 66.2


line stmt bran cond sub pod time code
1             ##----------------------------------------------------------------------------
2             ## MIME Email Builder - ~/lib/Mail/Make/Stream/QuotedPrint.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::QuotedPrint;
14             BEGIN
15             {
16 8     8   1457 use strict;
  8         17  
  8         305  
17 8     8   38 use warnings;
  8         13  
  8         564  
18 8     8   3759 warnings::register_categories( 'Mail::Make' );
19 8     8   46 use Mail::Make::Stream;
  8         28  
  8         75  
20 8     8   2584 use parent -norequire, qw( Mail::Make::Stream::Generic );
  8         17  
  8         52  
21 8     8   499 use vars qw( @EXPORT_OK $VERSION $EXCEPTION_CLASS $QuotedPrintError $DEBUG );
  8         15  
  8         691  
22 8     8   52 use Exporter qw( import );
  8         14  
  8         430  
23 8     8   63 use Mail::Make::Exception;
  8         14  
  8         68  
24 8     8   2123 use Encode ();
  8         17  
  8         272  
25 8     8   38 use Module::Generic::File::IO;
  8         25  
  8         70  
26 8         23 our @EXPORT_OK = qw( decode_qp encode_qp );
27 8         14 our $EXCEPTION_CLASS = 'Mail::Make::Exception';
28 8         17 our $VERSION = 'v0.3.1';
29 8         206 our $DEBUG = 0;
30             };
31              
32 8     8   49 use strict;
  8         26  
  8         255  
33 8     8   40 use warnings;
  8         13  
  8         9270  
34              
35             # decode( $from, $to [, %opts] )
36             # Decodes QP data from $from line-by-line and writes raw bytes to $to.
37             sub decode
38             {
39 5     5 1 7401 my $self = shift( @_ );
40 5         9 my $from = shift( @_ );
41 5         7 my $to = shift( @_ );
42 5         16 my $opts = $self->_get_args_as_hash( @_ );
43 5         31 my( $from_fh, $reader ) = $self->_get_glob_from_arg( $from );
44 5         12 my( $to_fh, $writer ) = $self->_get_glob_from_arg( $to, write => 1 );
45 5 50 33     24 return( $self->pass_error ) if( !defined( $from_fh ) || !defined( $to_fh ) );
46 5 50       18 $self->_load_class( 'MIME::QuotedPrint', { no_import => 1 } ) || return( $self->pass_error );
47              
48             # Wrap the filehandle into an object that supports getline().
49             # Native in-memory handles (fileno = -1) already support readline(); do NOT attempt
50             # fdopen() on them: that call requires a real OS file descriptor.
51 5 50       2323 unless( $self->_can( $from_fh => 'getline' ) )
52             {
53 0 0       0 my $fd = $self->_can( $from_fh => 'fileno' ) ? $from_fh->fileno : fileno( $from_fh );
54 0 0 0     0 if( defined( $fd ) && $fd >= 0 )
55             {
56 0         0 my $io = Module::Generic::File::IO->new;
57 0 0       0 $io->fdopen( $fd, 'r' ) ||
58             return( $self->pass_error( $io->error ) );
59 0         0 $from_fh = $io;
60             }
61             # else: native in-memory glob - readline() below works as-is
62             }
63              
64 5         78 my $buff;
65 5 50       11 while( defined( $buff = $self->_can( $from_fh => 'getline' ) ? $from_fh->getline : readline( $from_fh ) ) )
66             {
67 5         1802 my $decoded = MIME::QuotedPrint::decode_qp( $buff );
68 5         16 my $rv = $writer->( $decoded );
69 5 50       15 return( $self->pass_error ) if( !defined( $rv ) );
70             }
71 5 50 33     826 return( $self->pass_error( $from_fh->error ) )
      33        
72             if( !defined( $buff ) && $self->_can( $from_fh => 'error' ) && $self->error );
73 5         2007 return( $self );
74             }
75              
76             sub decode_qp
77             {
78 1     1 1 944 my $s = __PACKAGE__->new( debug => $DEBUG );
79 1         10 my $rv = $s->decode( @_ );
80 1 50       2784 if( !defined( $rv ) )
81             {
82 0         0 $QuotedPrintError = $s->error;
83 0         0 return( $s->pass_error );
84             }
85             else
86             {
87 1         4 undef( $QuotedPrintError );
88 1         79 return( $rv );
89             }
90             }
91              
92             # encode( $from, $to [, %opts] )
93             # Encodes raw data from $from as Quoted-Printable and writes to $to.
94             # Options:
95             # eol => $str line ending (default "\015\012")
96             sub encode
97             {
98 44     44 1 134 my $self = shift( @_ );
99 44         176 my $from = shift( @_ );
100 44         92 my $to = shift( @_ );
101 44         254 my $opts = $self->_get_args_as_hash( @_ );
102 44         1373 my( $from_fh, $reader ) = $self->_get_glob_from_arg( $from );
103 44         149 my( $to_fh, $writer ) = $self->_get_glob_from_arg( $to, write => 1 );
104 44 50 33     302 return( $self->pass_error ) if( !defined( $from_fh ) || !defined( $to_fh ) );
105 44 50       271 $self->_load_class( 'MIME::QuotedPrint', { no_import => 1 } ) || return( $self->pass_error );
106              
107             # Wrap the filehandle into an object that supports getline().
108             # Native in-memory handles (fileno = -1) already support readline(); do NOT attempt
109             # fdopen() on them: that call requires a real OS file descriptor.
110 44 100       41349 unless( $self->_can( $from_fh => 'getline' ) )
111             {
112 42 50       686 my $fd = $self->_can( $from_fh => 'fileno' ) ? $from_fh->fileno : fileno( $from_fh );
113 42 50 33     927 if( defined( $fd ) && $fd >= 0 )
114             {
115 0         0 my $io = Module::Generic::File::IO->new;
116 0 0       0 $io->fdopen( $fd, 'r' ) ||
117             return( $self->pass_error( $io->error ) );
118 0         0 $from_fh = $io;
119             }
120             # else: native in-memory glob - readline() below works as-is
121             }
122 44 100 66     271 my $eol = ( exists( $opts->{eol} ) && defined( $opts->{eol} ) ) ? $opts->{eol} : "\015\012";
123 44         190 my $has_eol = length( $eol );
124              
125 44         80 my $buff;
126 44 100       283 while( defined( $buff = $self->_can( $from_fh => 'getline' ) ? $from_fh->getline : readline( $from_fh ) ) )
127             {
128             # Ensure the chunk is raw UTF-8 bytes, not Perl's internal representation
129 47 50       1506 $buff = Encode::encode_utf8( $buff ) if( Encode::is_utf8( $buff ) );
130 47 50       368 my $encoded = MIME::QuotedPrint::encode_qp( $buff, ( $has_eol ? ( $eol ) : () ) );
131 47         181 my $rv = $writer->( $encoded );
132 47 50       205 return( $self->pass_error ) if( !defined( $rv ) );
133             }
134 44 50 66     883 return( $self->pass_error( $from_fh->error ) )
      66        
135             if( !defined( $buff ) && $self->_can( $from_fh => 'error' ) && $self->error );
136 44         1739 return( $self );
137             }
138              
139             sub encode_qp
140             {
141 1     1 1 5971 my $s = __PACKAGE__->new;
142 1         5 my $rv = $s->encode( @_ );
143 1 50       2589 if( !defined( $rv ) )
144             {
145 0         0 $QuotedPrintError = $s->error;
146 0         0 return( $s->pass_error );
147             }
148             else
149             {
150 1         2 undef( $QuotedPrintError );
151 1         3 return( $rv );
152             }
153             }
154              
155 0     0 1   sub encode_qp_utf8 { return( shift->encode_qp( Encode::encode_utf8( shift( @_ ) ) ) ); }
156              
157             sub is_decoder_installed
158             {
159 0     0 1   local $@;
160             eval
161 0           {
162 0     0     local $SIG{__DIE__} = sub{};
163 0           require MIME::QuotedPrint;
164             };
165 0 0         return( $@ ? 0 : 1 );
166             }
167              
168             sub is_encoder_installed
169             {
170 0     0 1   local $@;
171             eval
172 0           {
173 0     0     local $SIG{__DIE__} = sub{};
174 0           require MIME::QuotedPrint;
175             };
176 0 0         return( $@ ? 0 : 1 );
177             }
178              
179             # NOTE: sub FREEZE is inherited
180              
181             # NOTE: sub STORABLE_freeze is inherited
182              
183             # NOTE: sub STORABLE_thaw is inherited
184              
185             # NOTE: sub THAW is inherited
186              
187             1;
188             # NOTE: POD
189             __END__
190              
191             =encoding utf-8
192              
193             =head1 NAME
194              
195             Mail::Make::Stream::QuotedPrint - Streaming Quoted-Printable Encoder/Decoder for Mail::Make
196              
197             =head1 SYNOPSIS
198              
199             use Mail::Make::Stream::QuotedPrint;
200              
201             my $s = Mail::Make::Stream::QuotedPrint->new ||
202             die( Mail::Make::Stream::QuotedPrint->error, "\n" );
203              
204             $s->encode( $input => $output, eol => "\015\012" ) ||
205             die( $s->error );
206              
207             $s->decode( $input => $output ) || die( $s->error );
208              
209             use Mail::Make::Stream::QuotedPrint qw( encode_qp decode_qp );
210             encode_qp( $input => $output, eol => "\015\012" ) ||
211             die( $Mail::Make::Stream::QuotedPrint::QuotedPrintError );
212              
213             =head1 VERSION
214              
215             v0.3.0
216              
217             =head1 DESCRIPTION
218              
219             RFC 2045 Quoted-Printable encoder and decoder. Both L</encode> and L</decode> operate as line-oriented stream pipelines: data is read from C<$from> line by line (via C<getline()>) and written to C<$to> without accumulating the full content in memory. Suitable for large text parts backed by L<Mail::Make::Body::File>.
220              
221             Each of C<$from> and C<$to> may be:
222              
223             =over 4
224              
225             =item * A native filehandle or IO object
226              
227             =item * A scalar reference (C<\$scalar>)
228              
229             =item * A plain string (file path)
230              
231             =back
232              
233             =head1 METHODS
234              
235             =head2 decode( $from, $to )
236              
237             Reads QP-encoded data from C<$from> line by line, decodes each line via L<MIME::QuotedPrint>, and writes the raw bytes to C<$to>. Returns C<$self> on success, C<undef> on error.
238              
239             =head2 encode( $from, $to [, %opts] )
240              
241             Reads raw data from C<$from> line by line, encodes each line as Quoted-Printable, and writes the result to C<$to>. Any Perl-internal UTF-8 representation is converted to raw UTF-8 bytes via L<Encode/encode_utf8> before encoding. Returns C<$self> on success, C<undef> on error.
242              
243             Options:
244              
245             =over 4
246              
247             =item C<eol>
248              
249             Line ending appended after each encoded line. Defaults to CRLF (C<"\015\012">).
250              
251             =back
252              
253             =head1 CLASS FUNCTIONS
254              
255             The following functions are exportable on request:
256              
257             use Mail::Make::Stream::QuotedPrint qw( encode_qp decode_qp );
258              
259             =head2 encode_qp( $from, $to [, %opts] )
260              
261             Convenience wrapper for L</encode>. Sets C<$QuotedPrintError> and returns C<undef> on failure.
262              
263             =head2 decode_qp( $from, $to )
264              
265             Convenience wrapper for L</decode>. Sets C<$QuotedPrintError> and returns C<undef> on failure.
266              
267             =head2 encode_qp_utf8( $str )
268              
269             Encodes C<$str> as UTF-8 bytes first, then as Quoted-Printable.
270              
271             =head2 is_encoder_installed
272              
273             =head2 is_decoder_installed
274              
275             Return true if L<MIME::QuotedPrint> is available.
276              
277             =head1 AUTHOR
278              
279             Jacques Deguest E<lt>F<jack@deguest.jp>E<gt>
280              
281             =head1 SEE ALSO
282              
283             L<Mail::Make>, L<Mail::Make::Entity>, L<Mail::Make::Stream::Base64>, L<Mail::Make::Stream>, L<MIME::QuotedPrint>
284              
285             RFC 2045
286              
287             =head1 COPYRIGHT & LICENSE
288              
289             Copyright(c) 2026 DEGUEST Pte. Ltd.
290              
291             All rights reserved.
292              
293             This program is free software; you can redistribute it and/or modify it under the same terms as Perl itself.
294              
295             =cut