#!/usr/local/bin/perl # # Copyright (c) 1997-1998 Kevin Johnson . # # All rights reserved. This program is free software; you can # redistribute it and/or modify it under the same terms as Perl # itself. # # $Id: IMAP.pm,v 1.1 1998/04/05 15:40:46 kjj Exp $ require 5.004; package Net::IMAP; use strict; =head1 NAME Net::IMAP - A client interface to the Internet Message Access Protocol (IMAP). C =cut use IO::Socket; use Net::xAP qw($xAP_ATOM $xAP_ASTRING $xAP_PARENS); use Carp; use Data::Dumper; use vars qw($VERSION @ISA $SEQUENCE); @ISA = qw(Net::xAP IO::Socket::INET); =head1 SYNOPSIS C =head1 DESCRIPTION C provides a perl interface to the client portion of the Internet Message Access Protocol (IMAP). =head1 METHODS =cut my $Debug = 0; my %_system_flags = ( '\Seen' => 1, '\Answered' => 1, '\Flagged' => 1, '\Deleted' => 1, '\Recent' => 1, ); sub new { my $class = shift; my $type = ref($class) || $class; my $host = shift if @_ % 2; my %options = @_; $options{Debug} ||= $Debug; my $self = Net::xAP->new($host, 'imap2(143)', Timeout => 10, %options) or return undef; bless $self, $class; $self->{Options} = {%options}; $self->{PreAuth} = 0; $self->{Banner} = undef; $self->{Capabilities} = (); $self->{Mailbox} = (); $self->{DefaultCallback} = $self->default_callback; STDERR->autoflush(1); $self->_get_banner or return undef; my $resp; if (defined($self->{Options}{Synchronous}) && $self->{Options}{Synchronous}) { $resp = $self->capability; } else { $self->capability; $resp = $self->response; } if ($resp->status ne 'OK') { carp "capability command failed on initial connection"; $self->connection->close or carp "error closing connection: $!"; $! = 5; # *sigh* error reporting needs to be improved return undef; } return $self; } # hide the password from the debug output. sub debug_text { $_[2] =~ /^(\d+ LOGIN [^\s]+)/i ? "$1 ..." : $_[2] } sub _get_banner { my $self = shift; my $list = $self->getline or return undef; $self->debug_print(0, join(' ', @{$list})) if $self->debug; if (($list->[0] eq '*') && ($list->[1] =~ /^PREAUTH$/i)) { $self->{PreAuth}++; } elsif (($list->[0] ne '*') || ($list->[1] !~ /^OK$/i)) { return undef; } my $found_it = 0; for my $item (@{$list}) { $found_it++ if ($item =~ /^IMAP4/i); } unless ($found_it) { $self->connection->close; return undef; } $self->{Banner} = $list; return 1; } ############################################################################### sub preauth { return shift->{PreAuth} } sub banner { return shift->{Banner} } ############################################################################### # noop sub noop { $_[0]->command($_[0]->generic_callback, 'noop') } # capability sub capability { $_[0]->command($_[0]->capability_callback, 'capability') } sub has_capability { return defined($_[0]->{Capabilities}{$_[1]}) } # logout sub logout { $_[0]->command($_[0]->logout_callback, 'logout') } # authenticate AUTHTYPE @BASE64GOO # sub authenticate { shift->command(sub {0}, 'authenticate', @_) } # login USER PASSWORD sub login { $_[0]->command($_[0]->generic_callback, 'login', $xAP_ASTRING => $_[1], $xAP_ASTRING => $_[2]); } # select MAILBOX sub select { $_[0]->command($_[0]->select_callback, 'select', $xAP_ASTRING => $_[1]); } # examine MAILBOX sub examine { $_[0]->command($_[0]->select_callback, 'examine', $xAP_ASTRING => $_[1]); } # create MAILBOX sub create { $_[0]->command($_[0]->generic_callback, 'create', $xAP_ASTRING => $_[1]); } # delete MAILBOX sub delete { $_[0]->command($_[0]->generic_callback, 'delete', $xAP_ASTRING => $_[1]); } # rename MAILBOX MAILBOX sub rename { $_[0]->command($_[0]->generic_callback, 'rename', $xAP_ASTRING => $_[1], $xAP_ASTRING => $_[2]); } # subscribe MAILBOX sub subscribe { $_[0]->command($_[0]->generic_callback, 'subscribe', $xAP_ASTRING => $_[1]); } # unsubscribe MAILBOX sub unsubscribe { $_[0]->command($_[0]->generic_callback, 'unsubscribe', $xAP_ASTRING => $_[1]); } # list REFNAME REGEXMAILBOXLIST sub list { my $self = shift; my @args; push @args, $xAP_ASTRING => shift; # REFNAME for my $item (@_) { # REGEXMAILBOXLIST push @args, $xAP_ASTRING => $item; } $self->command($self->list_callback('LIST'), 'list', @args); } # lsub REFNAME @REGEXMAILBOXLIST sub lsub { my $self = shift; my @args; push @args, $xAP_ASTRING => shift; # REFNAME for my $item (@_) { # REGEXMAILBOXLIST push @args, $xAP_ASTRING => $item; } $self->command($self->list_callback('LSUB'), 'list', @args); } # status MAILBOX @STATUSATTRS sub status { my $self = shift; $self->command($self->status_callback($_[0]), 'status', $xAP_ASTRING => shift, $xAP_PARENS => [@_]); } # append MAILBOX [FLAGLIST] [DATETIMESTRING] MESSAGELITERAL sub append { my $self = shift; my $mailbox = shift; my $lit = shift; my %options = @_; my @args; push @args, $xAP_ASTRING => $mailbox; if (defined($options{Flags})) { for my $flag (@{$options{Flags}}) { unless ($self->_valid_flag($flag)) { carp "$flag is not a system flag"; return undef; } } push @args, $xAP_PARENS => [@{$options{Flags}}]; } push @args, $xAP_ATOM => "\"$options{Date}\"" if (defined($options{Date})); # the next was a problem for someone - not sure what the problem was # $lit =~ s/$/\r/mg; push @args, $xAP_ASTRING => $lit; $self->command($self->generic_callback, 'append', @args); } # check sub check { $_[0]->command($_[0]->generic_callback, 'check') } # close sub close { $_[0]->command($_[0]->close_callback, 'close') } # expunge sub expunge { $_[0]->command($_[0]->expunge_callback, 'expunge') } # search [charset $xAP_ASTRING] @SEARCHKEYS sub search { my $self = shift; my @args; if ($_[0] =~ /^CHARSET$/i) { shift; my $charset = shift; push @args, $xAP_ATOM => 'CHARSET', $xAP_ASTRING => $charset; } for my $item (@_) { push @args, $xAP_ATOM => $item; } $self->command($self->search_callback, 'search', @args); } # fetch MSGSET all|full|fast|FETCHATTR|@FETCHATTRS sub fetch { my $self = shift; my $msgset = shift; my @args; if (scalar(@_) == 1) { push @args, $xAP_ATOM => shift; } else { push @args, $xAP_PARENS => [@_]; } $self->command($self->fetch_callback, 'fetch', $xAP_ATOM => $msgset, @args); } # store MSGSET ITEMNAME @STOREATTRFLAGS sub store { my $self = shift; my $msgset = shift; my $itemname = shift; for my $flag (@_) { unless ($self->_valid_flag($flag)) { carp "$flag is not a system flag"; return undef; } } $self->command($self->store_callback, 'store', $xAP_ATOM => $msgset, $xAP_ATOM => $itemname, $xAP_PARENS => [@_]); } # copy MSGSET MAILBOX sub copy { $_[0]->command($_[0]->generic_callback, 'copy', $xAP_ATOM => $_[1], $xAP_ASTRING => $_[2]); } # uid copy MSGSET MAILBOX sub uid_copy { $_[0]->command($_[0]->generic_callback, 'uid copy', $xAP_ATOM => $_[1], $xAP_ASTRING => $_[2]); } # uid fetch MSGSET all|full|fast|FETCHATTR|@FETCHATTRS sub uid_fetch { my $self = shift; my $msgset = shift; my @args; if (scalar(@_) == 1) { push @args, $xAP_ATOM => shift; } else { push @args, $xAP_PARENS => [@_]; } $self->command($self->fetch_callback, 'uid fetch', $xAP_ATOM => $msgset, @args); } # uid search [charset $xAP_ASTRING] @SEARCHKEYS sub uid_search { my $self = shift; my @args; if ($_[0] =~ /^CHARSET$/i) { shift; my $charset = shift; push @args, $xAP_ATOM => 'CHARSET', $xAP_ASTRING => $charset; } for my $item (@_) { push @args, $xAP_ATOM => $item; } $self->command($self->search_callback, 'uid search', @args); } # uid store MSGSET ITEMNAME @STOREATTRFLAGS sub uid_store { my $self = shift; my $msgset = shift; my $itemname = shift; for my $flag (@_) { unless ($self->_valid_flag($flag)) { carp "$flag is not a system flag"; return undef; } } $self->command($self->store_callback, 'uid store', $xAP_ATOM => $msgset, $xAP_ATOM => $itemname, $xAP_PARENS => [@_]); } ############################################################################### # setacl MAILBOX IDENTIFIER MODRIGHTS sub setacl { my $self = shift; return undef unless $self->has_capability('ACL'); $self->command($self->generic_callback, 'setacl', $xAP_ASTRING => $_[0], $xAP_ASTRING => $_[1], $xAP_ASTRING => $_[2]); } # getacl MAILBOX sub getacl { my $self = shift; return undef unless $self->has_capability('ACL'); $self->command($self->generic_callback, 'getacl', $xAP_ASTRING => $_[0]); } sub getacl_callback { my $self = shift; my $seq = $self->next_sequence; my $resp = Net::IMAP::Response->new; return sub { my $list = shift; if (($list->[0] eq '*') && ($list->[1] =~ /^ACL$/i)) { my @list2 = @{$list}; my @sublist = @list2[2..$#list2] || (); $resp->{ACLS}{$list->[2]} = {@sublist}; } elsif ($list->[0] =~ /^\d+$/) { $resp->parse_status($list); return $resp; } else { return 0; } return 1; } } # deleteacl MAILBOX IDENTIFIER sub deleteacl { my $self = shift; return undef unless $self->has_capability('ACL'); $self->command($self->generic_callback, 'deleteacl', $xAP_ASTRING => $_[0], $xAP_ASTRING => $_[0]); } # listrights MAILBOX IDENTIFIER sub listrights { my $self = shift; return undef unless $self->has_capability('ACL'); $self->command($self->listrights_callback, 'listrights', $xAP_ASTRING => $_[0], $xAP_ASTRING => $_[0]); } sub listrights_callback { my $self = shift; my $seq = $self->next_sequence; my $resp = Net::IMAP::Response->new; return sub { my $list = shift; if (($list->[0] eq '*') && ($list->[1] =~ /^LISTRIGHTS$/i)) { my @list2 = @{$list}; $resp->{LISTRIGHTS}{$list->[2]}{$list->[3]} = [@list2[4..$#list2]]; } elsif ($list->[0] =~ /^\d+$/) { $resp->parse_status($list); return $resp; } else { return 0; } return 1; } } # myrights MAILBOX sub myrights { my $self = shift; return undef unless $self->has_capability('ACL'); $self->command($self->myrights_callback, 'myrights', $xAP_ASTRING => $_[0]); } sub myrights_callback { my $self = shift; my $seq = $self->next_sequence; my $resp = Net::IMAP::Response->new; return sub { my $list = shift; if (($list->[0] eq '*') && ($list->[1] =~ /^MYRIGHTS$/i)) { $resp->{MYRIGHTS}{$list->[2]} = $list->[3]; } elsif ($list->[0] =~ /^\d+$/) { $resp->parse_status($list); return $resp; } else { return 0; } return 1; } } ############################################################################### # getquota QUOTAROOT sub getquota { my $self = shift; return undef unless $self->has_capability('QUOTA'); $self->command($self->quota_callback, 'getquota', $xAP_ASTRING => $_[0]); } # setquota QUOTAROOT SETQUOTALIST sub setquota { my $self = shift; my $quotaroot = shift; return undef unless $self->has_capability('QUOTA'); $self->command($self->quota_callback, 'getquota', $xAP_ASTRING => $quotaroot, $xAP_PARENS => [@_]); } sub quota_callback { my $self = shift; my $seq = $self->next_sequence; my $resp = Net::IMAP::Response->new; return sub { my $list = shift; if (($list->[0] eq '*') && ($list->[1] =~ /^QUOTA$/i)) { $resp->{QUOTA}{$list->[2]} = $list->[3]; } elsif ($list->[0] =~ /^\d+$/) { $resp->parse_status($list); return $resp; } else { return 0; } return 1; } } # getquotaroot MAILBOX sub getquotaroot { my $self = shift; return undef unless $self->has_capability('QUOTA'); $self->command($self->getquotaroot_callback, 'getquotaroot', $xAP_ASTRING => $_[0]); } sub getquotaroot_callback { my $self = shift; my $seq = $self->next_sequence; my $resp = Net::IMAP::Response->new; return sub { my $list = shift; # TODO 1st arg after QUOTA and could be an astring # TODO args after QUOTAROOT and could be astrings if ($list->[0] eq '*') { my @list2 = @{$list}; if ($list->[1] =~ /^QUOTA$/i) { $resp->{QUOTA}{$list->[2]} = $list->[3]; } elsif ($list->[1] =~ /^QUOTAROOT$/i) { $resp->{QUOTAROOT} = $list->[2]; } else { return 0; } } elsif ($list->[0] =~ /^\d+$/) { $resp->parse_status($list); return $resp; } else { return 0; } return 1; } } ############################################################################### sub generic_callback { my $self = shift; my $seq = $self->next_sequence; my $resp = Net::IMAP::Response->new; return sub { my $list = shift; if (($list->[0] eq '*') && ($list->[1] =~ /^OK$/i) && ($list->[2] =~ /^\[TRYCREATE\]$/i)) { # eat it for now return 1; } if ($list->[0] =~ /^\d+$/) { $resp->parse_status($list); return $resp; } return 0; } } sub capability_callback { my $self = shift; my $seq = $self->next_sequence; my $resp = Net::IMAP::Response->new; return sub { my $list = shift; if (($list->[0] eq '*') && ($list->[1] =~ /^CAPABILITY$/i)) { my @list2 = @{$list}; for my $cap (@list2[2..$#list2]) { my $uccap = uc($cap); $self->{Capabilities}{$uccap}++; if ($uccap =~ /^AUTH=(.*)$/) { $self->{AuthTypes}{$1}++; } } } elsif ($list->[0] =~ /^\d+$/) { $resp->parse_status($list); return $resp; } else { return 0; } return 1; } } sub close_callback { my $self = shift; my $seq = $self->next_sequence; my $resp = Net::IMAP::Response->new; return sub { my $list = shift; if ($list->[0] =~ /^\d+$/) { $resp->parse_status($list); $self->{Mailbox} = () if ($resp->status eq 'OK'); return $resp; } return 0; } } sub select_callback { my $self = shift; my $seq = $self->next_sequence; my $resp = Net::IMAP::Response->new; # clean this up... $self->{Mailbox}{_EXISTS} = undef; $self->{Mailbox}{_RECENT} = undef; $self->{Mailbox}{_UIDVALIDITY} = undef; $self->{Mailbox}{_UNSEEN} = undef; $self->{Mailbox}{_PERMANENTFLAGS} = {}; $self->{Mailbox}{_FLAGS} = {}; return sub { my $list = shift; if ($list->[0] eq '*') { if (($list->[1] =~ /^\d+$/) && ($list->[2] =~ /^EXISTS$/i)) { $self->{Mailbox}{_EXISTS} = $list->[1]; } elsif (($list->[1] =~ /^\d+$/) && ($list->[2] =~ /^RECENT$/i)) { $self->{Mailbox}{_RECENT} = $list->[1]; } elsif ($list->[1] =~ /^OK$/i) { if ($list->[2] =~ /^\[UIDVALIDITY$/i) { $list->[3] =~ s/\]$//; $self->{Mailbox}{_UIDVALIDITY} = $list->[3]; } elsif ($list->[2] =~ /^\[UNSEEN$/i) { $list->[3] =~ s/\]$//; $self->{Mailbox}{_UNSEEN} = $list->[3]; } elsif ($list->[2] =~ /^\[PERMANENTFLAGS$/i) { for my $flag (@{$list->[3]}) { $self->{Mailbox}{_PERMANENTFLAGS}{$flag}++; } } else { return 0 } } elsif ($list->[1] =~ /^FLAGS$/i) { for my $flag (@{$list->[2]}) { $self->{Mailbox}{_FLAGS}{$flag}++; } } else { return 0 } } elsif ($list->[0] =~ /^\d+$/) { $resp->parse_status($list); if ($resp->status eq 'OK') { # we've saved several values from untagged responses into temp # locations until we knew that the command was OK. $self->{Mailbox}{EXISTS} = $self->{Mailbox}{_EXISTS}; delete $self->{Mailbox}{_EXISTS}; $self->{Mailbox}{RECENT} = $self->{Mailbox}{_RECENT}; delete $self->{Mailbox}{_RECENT}; $self->{Mailbox}{UIDVALIDITY} = $self->{Mailbox}{_UIDVALIDITY}; delete $self->{Mailbox}{_UIDVALIDITY}; $self->{Mailbox}{UNSEEN} = $self->{Mailbox}{_UNSEEN}; delete $self->{Mailbox}{_UNSEEN}; $self->{Mailbox}{PERMANENTFLAGS} = {%{$self->{Mailbox}{_PERMANENTFLAGS}}}; delete $self->{Mailbox}{_PERMANENTFLAGS}; $self->{Mailbox}{FLAGS} = {%{$self->{Mailbox}{_FLAGS}}}; delete $self->{Mailbox}{_FLAGS}; # only set this if the command was OK $self->{Mailbox}{READONLY} = ($resp->status_code eq 'READ-ONLY')?1:0; } return $resp; } else { return 0; } return 1; } } sub status_callback { my $self = shift; my $mailbox = shift; my $seq = $self->next_sequence; my $resp = Net::IMAP::Status->new; return sub { my $list = shift; if (($list->[0] eq '*') && ($list->[1] =~ /^STATUS$/i) && ($list->[2] eq $mailbox)) { $resp->{Status}{$mailbox} = {@{$list->[3]}}; } elsif ($list->[0] =~ /^\d$/) { $resp->parse_status($list); return $resp; } else { return 0; } return 1; } } sub expunge_callback { my $self = shift; my $mailbox = shift; my $seq = $self->next_sequence; my $resp = Net::IMAP::Expunge->new; return sub { my $list = shift; if (($list->[0] eq '*') && ($list->[1] =~ /^\d+$/) && ($list->[2] =~ /^EXPUNGE$/i)) { push @{$resp->{List}}, $list->[1]; } elsif ($list->[0] =~ /^\d+$/) { $resp->parse_status($list); return $resp; } else { return 0; } return 1; } } sub search_callback { my $self = shift; my $seq = $self->next_sequence; my $resp = Net::IMAP::Search->new; return sub { my $list = shift; if (($list->[0] eq '*') && ($list->[1] =~ /^SEARCH$/i)) { my @list2 = @{$list}; push @{$resp->{List}}, @list2[2..$#list2]; } elsif ($list->[0] =~ /^\d+$/) { $resp->parse_status($list); return $resp; } else { return 0; } return 1; } } sub store_callback { my $self = shift; my $seq = $self->next_sequence; my $resp = Net::IMAP::Store->new; return sub { my $list = shift; if (($list->[0] eq '*') && ($list->[1] =~ /^\d+$/) && ($list->[2] =~ /^FETCH$/i)) { my @list2 = @{$list->[3]}; if ($list2[0] =~ /^FLAGS$/i) { $resp->{Flags} = [@list2[1..$#list2]]; } } elsif ($list->[0] =~ /^\d+$/) { $resp->parse_status($list); return $resp; } else { return 0; } return 1; } } sub fetch_callback { my $self = shift; my $seq = $self->next_sequence; my $resp = Net::IMAP::Fetch->new; return sub { my $list = shift; if (($list->[0] eq '*') && ($list->[1] =~ /^\d+$/) && ($list->[2] =~ /^FETCH$/i)) { # $resp->{Data} = $list->[3]; push @{$resp->{Data}}, $list->[3]; } elsif ($list->[0] =~ /^\d+$/) { $resp->parse_status($list); return $resp; } else { return 0; } return 1; } } sub list_callback { my $self = shift; my $mode = shift; my $seq = $self->next_sequence; my $resp = Net::IMAP::List->new; return sub { my $list = shift; if (($list->[0] eq '*') && ($list->[1] =~ /^$mode$/i) && (ref($list->[2]) eq 'ARRAY')) { push @{$resp->{Folders}}, {Flags => \@{$list->[2]}, Delim => $list->[3], Name => $list->[4]}; } elsif ($list->[0] =~ /^\d+$/) { $resp->parse_status($list); return $resp; } else { return 0; } return 1; } } sub logout_callback { my $self = shift; my $seq = $self->next_sequence; my $resp = Net::IMAP::Response->new; return sub { my $list = shift; if (($list->[0] eq '*') && ($list->[1] =~ /^BYE$/i)) { # need to claim it, but don't need to do anything with it since we'll # be getting a tagged response RSN. } elsif ($list->[0] =~ /^\d+$/) { $resp->parse_status($list); $self->connection->close if ($resp->status eq 'OK'); return $resp; } else { return 0; } return 1; } } ############################################################################### sub default_callback { my $self = shift; return sub { my $list = shift; if ($list->[0] eq '*') { if ($list->[1] =~ /^\d+$/) { if ($list->[2] =~ /^EXISTS$/i) { $self->{Mailbox}{EXISTS} = $list->[1]; } elsif ($list->[2] =~ /^RECENT$/i) { $self->{Mailbox}{RECENT} = $list->[1]; } elsif ($list->[2] =~ /^EXPUNGE$/i) { # place holder; } else { return 0; } } elsif ($list->[1] =~ /^FLAGS$/i) { undef $self->{Mailbox}{FLAGS}; for my $flag (@{$list->[2]}) { $self->{Mailbox}{FLAGS}{$flag}++; } } elsif ($list->[1] =~ /^BYE$/i) { $self->connection->close; } elsif ($list->[1] =~ /^OK$/i) { if ($list->[2] =~ /^\[READ-(ONLY|WRITE)\]$/i) { $self->{Mailbox}{READONLY} = ($1 eq 'WRITE') ? 0 : 1; } elsif ($list->[2] =~ /^\[UIDVALIDITY$/i) { my $uidvalidity = $list->[3]; $uidvalidity =~ s/\]$//; $self->{Mailbox}{UIDVALIDITY} = $uidvalidity; } elsif ($list->[2] =~ /^\[UNSEEN$/i) { my $unseen = $list->[3]; $unseen =~ s/\]$//; $self->{Mailbox}{UNSEEN} = $unseen; } elsif ($list->[2] =~ /^\[PERMANENTFLAGS$/i) { undef $self->{Mailbox}{PERMANENTFLAGS}; for my $flag (@{$list->[3]}) { $self->{Mailbox}{PERMANENTFLAGS}{$flag}++; } } else { return 0; } } else { return 0; } } else { return 0; } return 1; } } ############################################################################### sub _valid_flag { return ((substr($_[1], 0, 1) ne "\\") || defined($_system_flags{$_[1]})); } ############################################################################### sub _dump_internals { print STDERR "----\n", Dumper($_[0]), "----\n" } ############################################################################### package Net::IMAP::Response; use vars qw(@ISA); @ISA = qw(Net::xAP::Response); ############################################################################### package Net::IMAP::List; use vars qw(@ISA); @ISA = qw(Net::IMAP::Response); ############################################################################### package Net::IMAP::Fetch; use vars qw(@ISA); @ISA = qw(Net::IMAP::Response); ############################################################################### package Net::IMAP::Status; use vars qw(@ISA); @ISA = qw(Net::IMAP::Response); ############################################################################### package Net::IMAP::Expunge; use vars qw(@ISA); @ISA = qw(Net::IMAP::Response); ############################################################################### package Net::IMAP::Search; use vars qw(@ISA); @ISA = qw(Net::IMAP::Response); ############################################################################### package Net::IMAP::Store; use vars qw(@ISA); @ISA = qw(Net::IMAP::Response); ############################################################################### 1;