File Coverage

blib/lib/Net/Doveadm.pm
Criterion Covered Total %
statement 84 100 84.0
branch 34 52 65.3
condition 8 15 53.3
subroutine 13 13 100.0
pod 3 3 100.0
total 142 183 77.6


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