File Coverage

blib/lib/Net/Doveadm.pm
Criterion Covered Total %
statement 85 97 87.6
branch 32 46 69.5
condition 8 15 53.3
subroutine 12 12 100.0
pod 3 3 100.0
total 140 173 80.9


line stmt bran cond sub pod time code
1             package Net::Doveadm;
2              
3 3     3   16339 use strict;
  3         40  
  3         153  
4 3     3   37 use warnings;
  3         8  
  3         354  
5              
6             =encoding utf-8
7              
8             =head1 NAME
9              
10             Net::Doveadm - Dovecot’s administrative interface protocol
11              
12             =head1 SYNOPSIS
13              
14             my $doveadm = Net::Doveadm->new(
15             io => $io_object,
16              
17             # Required for authentication,
18             # but we warn if the server doesn’t ask for them.
19             username => $username,
20             password => $password,
21             );
22              
23             $doveadm->send(
24             username => $cmd_username,
25             command => [ $cmd, @args ],
26             );
27              
28             my $result_ar;
29              
30             {
31             # If using non-blocking I/O …
32             # $io_object->flush_write_queue();
33              
34             last if $result_ar = $doveadm->receive();
35              
36             # If using non-blocking I/O, put a select(), poll(),
37             # or similar call here.
38              
39             redo;
40             }
41              
42             =head1 DESCRIPTION
43              
44             This module implements client logic for the
45             L,
46             which facilitates administrative communication with a
47             L server via TCP or a local socket.
48              
49             =head1 I/O
50              
51             This module is designed to use L as an abstraction over
52             the transmission medium. It is expected that most implementors will use
53             this module; however, if so desired, a compatible interface can be
54             substituted. In particular, the object must implement the C
55             and C methods of that class.
56              
57             If you use L, you should B enable its C
58             mode. The Doveadm protocol is strictly RPC-oriented, which means the only
59             successful end to a session is one that the client terminates.
60              
61             Note that blocking and non-blocking I/O work nearly the same way;
62             the SYNOPSIS above demonstrates the difference. A particular feature of
63             this setup is that it’s possible to send multiple successive requests before
64             reading responses to those requests.
65              
66             =cut
67              
68             our $VERSION = '0.01-TRIAL1';
69              
70             use constant {
71 3         5275 _flag_verbose => 'v',
72             _flag_debug => 'D',
73 3     3   44 };
  3         36  
74              
75             our $DEBUG = 0;
76              
77             my $LF = "\x0a";
78              
79             =head1 METHODS
80              
81             =head2 Inew( %OPTS )
82              
83             Instantiates this class. %OPTS are:
84              
85             =over
86              
87             =item * C - An instance of L or a compatible interface.
88              
89             =item * C - The username to use in authentication. Required if
90             the server asks for it; if given and the server does not ask for it, a
91             warning is given.
92              
93             =item * C - As with C.
94              
95             =back
96              
97             Note that no I/O happens in this method.
98              
99             =cut
100              
101             sub new {
102 3     3 1 7841 my ($class, %opts) = @_;
103              
104 3         38 $opts{"_$_"} = delete $opts{$_} for keys %opts;
105 3         10 $opts{'_requests'} = [];
106              
107 3         11 return bless \%opts, $class;
108             }
109              
110             #----------------------------------------------------------------------
111              
112             =head2 I->send( %OPTS )
113              
114             Send (or enqueue the sending of) a command. %OPTS are:
115              
116             =over
117              
118             =item * C - The username to send in the command.
119              
120             =item * C - An array reference whose contents are (in order)
121             the command name and all arguments to the command.
122              
123             =back
124              
125             Note that, if the server handshake is not yet complete, this will
126             attempt to finish that before actually sending a message.
127              
128             =cut
129              
130             sub send {
131 3     3 1 70 my ($self, %opts) = @_;
132              
133 3         7 my $flags = q<>;
134 3 50       42 if ($opts{'flags'}) {
135 3         8 for my $flag ( @{ $opts{'flags'} } ) {
  3         15  
136 6 50       82 if (my $chr = __PACKAGE__->can("_flag_$flag")) {
137 6         20 $flags .= $chr->();
138             }
139             else {
140 0         0 die "Unknown flag: “$flag”";
141             }
142             }
143             }
144              
145 3         11 _validate_command_pieces( $opts{'username'}, $opts{'command'} );
146              
147 3         5 push @{ $self->{'_requests'} }, [ $flags, $opts{'username'}, @{ $opts{'command'} } ];
  3         13  
  3         10  
148              
149 3 50       13 if (!$self->{'_handshake_done'}) {
150 3 50       8 return $self if !$self->_do_handshake();
151             }
152              
153 0         0 $self->_flush_request_queue();
154              
155 0         0 return $self;
156             }
157              
158             sub _validate_command_pieces {
159 3     3   9 my ($username, $command_ar) = @_;
160              
161 3         12 for my $piece ($username, @$command_ar) {
162 12 50       34 if ($piece =~ tr<\t\x0a><>) {
163 0         0 die "Invalid string in command: “$piece”";
164             }
165             }
166              
167 3         7 return;
168             }
169              
170             #----------------------------------------------------------------------
171              
172             =head2 $RESULT = I->receive()
173              
174             Looks for a response to a previously-sent command. If such a response is
175             ready,
176             it will be returned as an array reference; otherwise, undef is returned.
177              
178             Note that, if the server handshake is not yet complete, this will
179             attempt to finish that before actually trying to retrieve a message.
180              
181             =cut
182              
183             sub receive {
184 26     26 1 13349 my ($self) = @_;
185              
186 26 100       117 if (!$self->{'_handshake_done'}) {
187 23 100       55 return undef if !$self->_do_handshake();
188              
189             # If we just finished the handshake, then send any pending requests
190             # before we see about responses to them.
191 3         11 $self->_flush_request_queue();
192             }
193              
194 6 0 33     11 if ( !@{ $self->{'_requests'} } && !$self->{'_sent_requests'} ) {
  6         19  
195 0         0 die "No requests pending!";
196             }
197              
198 6 100 66     26 $self->{'_line1'} ||= $self->_read_line() or return undef;
199              
200 3 50 33     19 $self->{'_line2'} ||= $self->_read_line() or return undef;
201              
202 3         11 $self->{'_sent_requests'}--;
203              
204 3         20 my ($line1, $line2) = delete @{$self}{'_line1', '_line2'};
  3         14  
205 3         13 chop( $line1, $line2 );
206              
207 3 50       14 if ($line2 ne '+') {
208 0         0 die "Error: $line2 ($line1)";
209             }
210              
211 3         23 return [ split m<\t>, $line1, -1 ];
212             }
213              
214             #----------------------------------------------------------------------
215              
216             sub _flush_request_queue {
217 3     3   18 my ($self) = @_;
218              
219 3         24 while ($self->_write($self->{'_requests'}[0])) {
220 0         0 shift @{ $self->{'_requests'} };
  0         0  
221 0         0 $self->{'_sent_requests'}++;
222             }
223              
224 3         125 return;
225             }
226              
227             sub _do_handshake {
228 26     26   47 my ($self) = @_;
229              
230 26 100       115 if (!$self->{'_sent_hello'}) {
231 3         52 $self->_write( [ 'VERSION', 'doveadm-server', 1, 0 ] );
232 3         1383 $self->{'_sent_hello'} = 1;
233 3         18 return undef;
234             }
235              
236 23 100 100     73 $self->{'_received_hello'} ||= $self->_read_line() or return undef;
237              
238 4 100       23 if ($self->{'_received_hello'} eq "+$LF") {
    50          
239 2         5 $self->{'_handshake_done'} = 1;
240              
241 2         6 for my $key ( qw( username password ) ) {
242 4 50       19 if ($self->{"_$key"}) {
243 0         0 warn "“$key” submitted, but server says unneeded.";
244             }
245             }
246             }
247             elsif ($self->{'_received_hello'} eq "-$LF") {
248              
249 2 100       6 if (!$self->{'_authn_sent'}) {
250 1         3 $self->_send_authn();
251              
252 1         3 $self->{'_authn_sent'} = 1;
253              
254 1         6 return undef;
255             }
256              
257 1 50 33     8 $self->{'_received_authn'} ||= $self->_read_line() or return undef;
258              
259 1 50       5 if ($self->{'_received_authn'} eq "+$LF") {
260              
261 1         3 $self->{'_handshake_done'} = 1;
262             }
263             else {
264 0         0 die "Failed authn: “$self->{'_received_authn'}”";
265             }
266             }
267              
268 3         8 return 1;
269             }
270              
271             sub _send_authn {
272 1     1   2 my ($self) = @_;
273              
274 1         3 for my $key ( qw( username password ) ) {
275 2 50       6 if (!length $self->{"_$key"}) {
276 0         0 die "“$key” not submitted, but server says needed!";
277             }
278             }
279              
280 1         570 require MIME::Base64;
281 1         1171 my $authn_b64 = MIME::Base64::encode_base64("\0" . $self->{'_username'} . "\0" . $self->{'_password'});
282 1         2 chop $authn_b64;
283              
284 1         5 $self->_write( [ 'PLAIN', $authn_b64 ] );
285              
286 1         41 return;
287             }
288              
289             sub _write {
290 7     7   20 my ($self, $pieces_ar) = @_;
291              
292 7 100       133 $DEBUG && print "$$ doveadm enqueue send: [@$pieces_ar]\n";
293              
294 7         74 return $self->{'_io'}->write( join("\t", @$pieces_ar ) . $LF );
295             }
296              
297             my $line_sr;
298              
299             sub _read_line {
300 32     32   53 my ($self) = @_;
301              
302 32         166 $line_sr = \$self->{'_io'}->read_until($LF);
303              
304 32 100       2225 if ($DEBUG) {
305 5 100       11 if ($$line_sr) {
306 4         10 chop $$line_sr;
307              
308 4         136 printf "$$ doveadm received: [$$line_sr]$LF";
309 4         50 return "$$line_sr$LF";
310             }
311             else {
312 1         12 printf "$$ no line yet fully received\n";
313             }
314             }
315              
316 28         134 return $$line_sr;
317             }
318              
319             #----------------------------------------------------------------------
320              
321             =head1 REPOSITORY
322              
323             L
324              
325             =head1 AUTHOR
326              
327             Felipe Gasper (FELIPE)
328              
329             =head1 COPYRIGHT
330              
331             Copyright 2018 by L
332              
333             =head1 LICENSE
334              
335             This distribution is released under the same license as Perl.
336              
337             =cut
338              
339             1;