File Coverage

blib/lib/Protocol/IMAP/Server.pm
Criterion Covered Total %
statement 9 9 100.0
branch n/a
condition n/a
subroutine 3 3 100.0
pod n/a
total 12 12 100.0


line stmt bran cond sub pod time code
1             package Protocol::IMAP::Server;
2             {
3             $Protocol::IMAP::Server::VERSION = '0.004';
4             }
5 1     1   28081 use strict;
  1         2  
  1         31  
6 1     1   5 use warnings;
  1         1  
  1         25  
7 1     1   741 use parent qw{Protocol::IMAP};
  1         274  
  1         4  
8              
9             =head1 NAME
10              
11             Protocol::IMAP::Server - server support for the Internet Message Access Protocol.
12              
13             =head1 VERSION
14              
15             version 0.004
16              
17             =head1 SYNOPSIS
18              
19             package Example::IMAP::Server;
20             use parent qw{Protocol::IMAP::Server};
21              
22             package main;
23             Example::IMAP::Server->new;
24              
25             =head1 DESCRIPTION
26              
27              
28             =head1 IMPLEMENTING SUBCLASSES
29              
30             The L classes only provides the framework for handling IMAP data. Typically you would need to subclass these to get a usable IMAP implementation.
31              
32             The following methods are required:
33              
34             =over 4
35              
36             =item * write - called at various points to send data back across to the other side of the IMAP connection
37              
38             =back
39              
40             and just about anything relating to the storage and handling of messages.
41              
42             =cut
43              
44             =head2 new
45              
46             =cut
47              
48             sub new {
49             my $class = shift;
50             my $self = bless { @_ }, $class;
51             return $self;
52             }
53              
54             sub on_connect {
55             my $self = shift;
56             $self->send_untagged("OK", "Net::Async::IMAP::Server ready.");
57             $self->state(Protocol::IMAP::ConnectionEstablished);
58             }
59              
60             sub send_untagged {
61             my ($self, $cmd, @data) = @_;
62             $self->debug("Send untagged command $cmd");
63             $self->write("* $cmd" . (@data ? join(' ', '', @data) : '') . "\n");
64             }
65              
66             sub send_tagged {
67             my ($self, $id, $status, @data) = @_;
68             $self->debug("Send tagged command $status for $id");
69             $self->write("$id $status" . (@data ? join(' ', '', @data) : '') . "\n");
70             }
71              
72             =head2 read_command
73              
74             Read a command from a single line input from the client.
75              
76             If this is a supported command, calls the relevant request_XXX method with the following data as a hash:
77              
78             =over 4
79              
80             =item * tag - IMAP tag information for this command, used for the final response from the server
81              
82             =item * command - actual command requested
83              
84             =item * param - any additional parameters passed after the command
85              
86             =back
87              
88             =cut
89              
90             sub read_command {
91             my $self = shift;
92             my $data = shift;
93             my ($id, $cmd, $param) = split / /, $data, 3;
94             my $method = "request_" . lc $cmd;
95             if($self->can($method)) {
96             return $self->$method(
97             id => $id,
98             command => $cmd,
99             param => $param
100             );
101             } else {
102             return $self->send_tagged($id, 'BAD', 'wtf dude');
103             }
104             }
105              
106             =head2 request_capability
107              
108             Request a list of all capabilities provided by the server.
109              
110             These will be returned in a single untagged response, followed by the usual status response.
111              
112             Note that the capabilities may vary depending on the state of the connection - for example, before STARTTLS negotiation
113             all login types may be disabled via LOGINDISABLED capability.
114              
115             =cut
116              
117             sub request_capability {
118             my $self = shift;
119             my %args = @_;
120             if(length $args{param}) {
121             $self->send_tagged($args{id}, 'BAD', 'Extra parameters detected');
122             } else {
123             $self->send_untagged('CAPABILITY', @{$self->{capabilities}});
124             $self->send_tagged($args{id}, 'OK', 'Capability completed');
125             }
126             }
127              
128             =head2 request_starttls
129              
130             Instructs the client to begin STARTTLS negotiation.
131              
132             All implementations should provide this.
133              
134             =cut
135              
136             sub request_starttls {
137             my $self = shift;
138             my %args = @_;
139             if(!$self->can('on_starttls')) {
140             $self->send_tagged($args{id}, 'BAD', 'Unknown command');
141             } elsif(length $args{param}) {
142             $self->send_tagged($args{id}, 'BAD', 'Extra parameters detected');
143             } else {
144             $self->send_tagged($args{id}, 'OK', 'Begin TLS negotiation now.');
145             $self->on_starttls;
146             }
147             }
148              
149             =head2 request_authenticate
150              
151             Requests SASL authentication. Didn't need it, haven't written it yet.
152              
153             =cut
154              
155             sub request_authenticate {
156             my $self = shift;
157             my %args = @_;
158             if(0) {
159             my ($user, $pass);
160             my $sasl = Authen::SASL->new(
161             mechanism => $args{param},
162             callback => {
163             # TODO Convert these to plain values or sapped entries
164             pass => sub { $pass },
165             user => sub { $user },
166             authname => sub { warn @_; }
167             }
168             );
169             my $s = $sasl->server_new(
170             'imap',
171             $self->server_name,
172             0,
173             );
174             }
175             $self->send_tagged($args{id}, 'NO', 'Not yet supported.');
176             }
177              
178             =head2 is_authenticated
179              
180             Returns true if we are authenticated, false if not.
181              
182             =cut
183              
184             sub is_authenticated {
185             my $self = shift;
186             return $self->state == Protocol::IMAP::Authenticated || $self->state == Protocol::IMAP::Selected;
187             }
188              
189             =head2 request_login
190              
191             Process a login request - this will be delegated to the subclass L method.
192              
193             =cut
194              
195             sub request_login {
196             my $self = shift;
197             my %args = @_;
198             $self->debug("Param was [" . $args{param} . "]");
199             my ($user, $pass) = split ' ', $args{param}, 2;
200             if($self->validate_user(user => $user, pass => $pass)) {
201             $self->state(Protocol::IMAP::Authenticated);
202             $self->send_tagged($args{id}, 'OK', 'Logged in.');
203             } else {
204             $self->send_tagged($args{id}, 'NO', 'Invalid user or password.');
205             }
206             }
207              
208             =head2 request_logout
209              
210             Process a logout request.
211              
212             =cut
213              
214             sub request_logout {
215             my $self = shift;
216             my %args = @_;
217             if(length $args{param}) {
218             $self->send_tagged($args{id}, 'BAD', 'Extra parameters detected');
219             } else {
220             $self->send_untagged('BYE', 'IMAP4rev1 server logging out');
221             $self->state(Protocol::IMAP::NotAuthenticated);
222             $self->send_tagged($args{id}, 'OK', 'Logout completed.');
223             }
224             }
225              
226             =head2 request_noop
227              
228             Handle a NOOP, which leaves state unchanged other than resetting any timers (as handled by the L method).
229              
230             =cut
231              
232             sub request_noop {
233             my $self = shift;
234             my %args = @_;
235             if(length $args{param}) {
236             $self->send_tagged($args{id}, 'BAD', 'Extra parameters detected');
237             } else {
238             $self->send_tagged($args{id}, 'OK', 'NOOP completed');
239             }
240             }
241              
242             =head2 request_select
243              
244             Select a mailbox.
245              
246             =cut
247              
248             sub request_select {
249             my $self = shift;
250             my %args = @_;
251             unless($self->is_authenticated) {
252             return $self->send_tagged($args{id}, 'NO', 'Not authorized.');
253             }
254             if(my $mailbox = $self->select_mailbox(mailbox => $args{param}, readonly => 1)) {
255             $self->send_mailbox_info($mailbox);
256             $self->send_tagged($args{id}, 'OK', 'Mailbox selected.');
257             } else {
258             $self->send_tagged($args{id}, 'NO', 'Mailbox not found.');
259             }
260             }
261              
262             =head2 C
263              
264             Return untagged information about the selected mailbox.
265              
266             =cut
267              
268             sub send_mailbox_info {
269             my ($self, $mailbox) = @_;
270             $self->send_untagged(exists $mailbox->{'exists'} ? $mailbox->{'exists'} : 0, 'EXISTS');
271             $self->send_untagged(exists $mailbox->{'recent'} ? $mailbox->{'recent'} : 0, 'RECENT');
272             $self->send_untagged('OK', '[UNSEEN ' . ($mailbox->{'first_unseen'} || 0) . ']', 'First unseen message ID');
273             $self->send_untagged('OK', '[UIDVALIDITY ' . ($mailbox->{'uid_valid'} || 0) . ']', 'Valid UIDs');
274             $self->send_untagged('OK', '[UIDNEXT ' . ($mailbox->{'uid_next'} || 0) . ']', 'Predicted next UID');
275             $self->send_untagged('FLAGS', '(\Answered \Flagged \Deleted \Seen \Draft)');
276             }
277              
278             =head2 request_examine
279              
280             Select a mailbox, in readonly mode.
281              
282             =cut
283              
284             sub request_examine {
285             my $self = shift;
286             my %args = @_;
287             unless($self->is_authenticated) {
288             return $self->send_tagged($args{id}, 'NO', 'Not authorized.');
289             }
290             if(my $mailbox = $self->select_mailbox(mailbox => $args{param}, readonly => 1)) {
291             $self->send_mailbox_info($mailbox);
292             $self->send_tagged($args{id}, 'OK', 'Mailbox selected.');
293             } else {
294             $self->send_tagged($args{id}, 'NO', 'Mailbox not found.');
295             }
296             }
297              
298             =head2 request_create
299              
300             Create a new mailbox.
301              
302             =cut
303              
304             sub request_create {
305             my $self = shift;
306             my %args = @_;
307             unless($self->is_authenticated) {
308             return $self->send_tagged($args{id}, 'NO', 'Not authorized.');
309             }
310             if(my $mailbox = $self->create_mailbox(mailbox => $args{param})) {
311             $self->send_tagged($args{id}, 'OK', 'Mailbox created.');
312             } else {
313             $self->send_tagged($args{id}, 'NO', 'Unable to create mailbox.');
314             }
315             }
316              
317             =head2 request_delete
318              
319             Delete a given mailbox.
320              
321             =cut
322              
323             sub request_delete {
324             my $self = shift;
325             my %args = @_;
326             unless($self->is_authenticated) {
327             return $self->send_tagged($args{id}, 'NO', 'Not authorized.');
328             }
329             if(my $mailbox = $self->delete_mailbox(mailbox => $args{param})) {
330             $self->send_tagged($args{id}, 'OK', 'Mailbox deleted.');
331             } else {
332             $self->send_tagged($args{id}, 'NO', 'Unable to delete mailbox.');
333             }
334             }
335              
336             =head2 request_rename
337              
338             Request renaming a mailbox to something else.
339              
340             =cut
341              
342             sub request_rename {
343             my $self = shift;
344             my %args = @_;
345             unless($self->is_authenticated) {
346             return $self->send_tagged($args{id}, 'NO', 'Not authorized.');
347             }
348             my ($src, $dst) = split ' ', $args{param}, 2;
349             if(my $mailbox = $self->rename_mailbox(mailbox => $src, target => $dst)) {
350             $self->send_tagged($args{id}, 'OK', 'Mailbox renamed.');
351             } else {
352             $self->send_tagged($args{id}, 'NO', 'Unable to rename mailbox.');
353             }
354             }
355              
356             =head2 request_subscribe
357              
358             Ask to subscribe to a mailbox.
359              
360             =cut
361              
362             sub request_subscribe {
363             my $self = shift;
364             my %args = @_;
365             unless($self->is_authenticated) {
366             return $self->send_tagged($args{id}, 'NO', 'Not authorized.');
367             }
368             if(my $mailbox = $self->subscribe_mailbox(mailbox => $args{param})) {
369             $self->send_tagged($args{id}, 'OK', 'Subscribed to mailbox.');
370             } else {
371             $self->send_tagged($args{id}, 'NO', 'Unable to subscribe to mailbox.');
372             }
373             }
374              
375             =head2 request_unsubscribe
376              
377             Ask to unsubscribe from a mailbox.
378              
379             =cut
380              
381             sub request_unsubscribe {
382             my $self = shift;
383             my %args = @_;
384             unless($self->is_authenticated) {
385             return $self->send_tagged($args{id}, 'NO', 'Not authorized.');
386             }
387             if(my $mailbox = $self->unsubscribe_mailbox(mailbox => $args{param})) {
388             $self->send_tagged($args{id}, 'OK', 'Unsubscribed from mailbox.');
389             } else {
390             $self->send_tagged($args{id}, 'NO', 'Unable to unsubscribe from mailbox.');
391             }
392             }
393              
394             =head2 request_list
395              
396             List mailboxes matching a specification.
397              
398             =cut
399              
400             sub request_list {
401             my $self = shift;
402             my %args = @_;
403             unless($self->is_authenticated) {
404             return $self->send_tagged($args{id}, 'NO', 'Not authorized.');
405             }
406             if(my $status = $self->list_mailbox(mailbox => $args{param})) {
407             $self->send_tagged($args{id}, 'OK', 'List completed.');
408             } else {
409             $self->send_tagged($args{id}, 'NO', 'Failed to list mailboxes.');
410             }
411             }
412              
413             =head2 request_lsub
414              
415             List subscriptions matching a spec - see L for more details on how this is implemented.
416              
417             =cut
418              
419             sub request_lsub {
420             my $self = shift;
421             my %args = @_;
422             unless($self->is_authenticated) {
423             return $self->send_tagged($args{id}, 'NO', 'Not authorized.');
424             }
425             if(my $status = $self->list_subscription(mailbox => $args{param})) {
426             $self->send_tagged($args{id}, 'OK', 'List completed.');
427             } else {
428             $self->send_tagged($args{id}, 'NO', 'Failed to list subscriptions.');
429             }
430             }
431              
432             =head2 on_multi_line
433              
434             Called when we have multi-line data (fixed size in characters).
435              
436             =cut
437              
438             sub on_multi_line {
439             my ($self, $data) = @_;
440              
441             if($self->{multiline}->{remaining}) {
442             $self->{multiline}->{buffer} .= $data;
443             $self->{multiline}->{remaining} -= length($data);
444             } else {
445             $self->{multiline}->{on_complete}->($self->{multiline}->{buffer});
446             delete $self->{multiline};
447             }
448             return $self;
449             }
450              
451             =head2 on_single_line
452              
453             Called when there's more data to process for a single-line (standard mode) response.
454              
455             =cut
456              
457             sub on_single_line {
458             my ($self, $data) = @_;
459              
460             $data =~ s/[\r\n]+//g;
461             $self->debug("Had [$data]");
462             $self->read_command($data);
463             return 1;
464             }
465              
466             sub is_multi_line { shift->{multiline} ? 1 : 0 }
467              
468             =head2 configure
469              
470             Set up any callbacks that were available.
471              
472             =cut
473              
474             sub configure {
475             my $self = shift;
476             my %args = @_;
477             foreach (Protocol::IMAP::STATE_HANDLERS, qw{on_idle_update}) {
478             $self->{$_} = delete $args{$_} if exists $args{$_};
479             }
480             $self->{capabilities} = [qw{IMAP4rev1 IDLE AUTH=LOGIN AUTH=PLAIN}];
481             return %args;
482             }
483              
484             =head2 add_capability
485              
486             Add a new capability to the reported list.
487              
488             =cut
489              
490             sub add_capability {
491             my $self = shift;
492             push @{$self->{capabilities}}, @_;
493             }
494              
495             =head2 validate_user
496              
497             Validate the given user and password information, returning true if they have logged in successfully
498             and false if they are invalid.
499              
500             =cut
501              
502             sub validate_user {
503             my $self = shift;
504             my %args = @_;
505             return 0;
506             }
507              
508             =head2 select_mailbox
509              
510             Selects the given mailbox.
511              
512             Expects a hashref indicating mailbox information, e.g.:
513              
514             my $mailbox = {
515             name => $args{mailbox},
516             exists => 17,
517             recent => 2,
518             };
519             return $mailbox;
520              
521             =cut
522              
523             sub select_mailbox {
524             my $self = shift;
525             my %args = @_;
526             return;
527             }
528              
529             =head2 create_mailbox
530              
531             Creates the given mailbox on the server.
532              
533             =cut
534              
535             sub create_mailbox {
536             my $self = shift;
537             my %args = @_;
538             }
539              
540             =head2 delete_mailbox
541              
542             Deletes the given mailbox.
543              
544             =cut
545              
546             sub delete_mailbox {
547             my $self = shift;
548             my %args = @_;
549             }
550              
551             =head2 rename_mailbox
552              
553             Renames the given mailbox.
554              
555             =cut
556              
557             sub rename_mailbox {
558             my $self = shift;
559             my %args = @_;
560             }
561              
562             =head2 subscribe_mailbox
563              
564             Adds the given mailbox to the active subscription list.
565              
566             =cut
567              
568             sub subscribe_mailbox {
569             my $self = shift;
570             my %args = @_;
571             }
572              
573             =head2 unsubscribe_mailbox
574              
575             Removes the given mailbox from the current user's subscription list.
576              
577             =cut
578              
579             sub unsubscribe_mailbox {
580             my $self = shift;
581             my %args = @_;
582             }
583              
584             =head2 list_mailbox
585              
586             List mailbox information given a search spec.
587              
588             =cut
589              
590             sub list_mailbox {
591             my $self = shift;
592             my %args = @_;
593             }
594              
595             =head2 list_subscription
596              
597             List subscriptions given a search spec.
598              
599             =cut
600              
601             sub list_subscription {
602             my $self = shift;
603             my %args = @_;
604             }
605              
606             1;