# Mail::vpopmail.pm
# $Id: vpopmail.pm,v 0.60b3 2007/04/16 00:32:24 jkister Exp $
# Copyright (c) 2004-2007 Jeremy Kister.
# Released under Perl's Artistic License.

$Mail::vpopmail::VERSION = "0.60b3";

=head1 NAME

Mail::vpopmail - Utility to get information about vpopmail managed email addresses

=head1 SYNOPSIS

use Mail::vpopmail;

my $vchkpw = Mail::vpopmail->new();

my $vchkpw = Mail::vpopmail->new(cache => 1,
                                 debug => 0,
                                 auth_module => 'cdb',
                                 dsn   => 'DBI:mysql:host=localhost;database=vpopmail',
                                 dbun  => 'vpopmailuser',
                                 dbpw  => 'vpoppasswd',
                                );

	
=head1 DESCRIPTION

C<Mail::vpopmail> provides serveral functions for interacting with
vpopmail.  This module can be useful especially when hashing is turned
on, as you can not predict the location of the domain's nor the 
mailbox's directories.

=head1 CONSTRUCTOR

=over 4

=item new( [OPTIONS] );

C<OPTIONS> are passed in a hash like fashion, using key and value
pairs.  Possible options are:

B<cache> - Cache results of queries (0=Off, 1=On).  Default=On.

B<debug> - Print debugging info to STDERR (0=Off, 1=On).  Default=On.

B<auth_module> - cdb or sql.  Default=cdb, but
			        Default=sql if ~vpopmail/etc/vpopmail.mysql exists.

B<dsn> - SQL DSN.  Default='DBI:mysql:host=localhost;database=vpopmail'
         Autogenerated if ~vpopmail/etc/vpopmail.mysql exists.

B<dbun> - SQL Username.  Default=vpopmailuser.
          Autogenerated if ~vpopmail/etc/vpopmail.mysql exists.

B<dbpw> - SQL Password.  Default=vpoppasswd.
          Autogenerated if ~vpopmail/etc/vpopmail.mysql exists.

=item userinfo( email => $email, field => <fields> );

B<email> - the email address to get properties on

B<field> - the field(s) to be returned (may be comma separated):

	dir - return this domain's vpopmail domains directory

	crypt - return the encrypted password

	uid - return the uid

	gid - return the gid

	comment - return the comment, if available

	maildir - return this user's maildir
	
	quota - return the quota (you have to parse this yourself)

	plain - return the plain text password, if available

=item domaininfo( domain => $domain, field => <field> );

B<domain> - the domain to get properties on

B<field> - the field to be returned:

   dir - return the vpopmail domain directory

   mailboxes - return an array reference containing all the mailboxes

   all - return an array ref of hash refs of all data for the domain
	
=item alldomains( field => <field> );

B<field> - the field to be returned:

   name - returns an array reference of the names of all domains

   dir - returns an array refrence of all domain directories

   map - returns a hash reference of domain name -> domain directory


=head1 EXAMPLES

	use strict;
	use Mail::vpopmail;

	my $vchkpw = Mail::vpopmail->new(cache=>1, debug=>0);


	# find all domains
	my $domains_aref = $vchkpw->alldomains(field => 'name');
	foreach my $domain (@${domains_aref}){
		print "$domain\n";
	}

	# find all domains directories
	my $dirlist_aref = $vchkpw->alldomains(field => 'dir');
	foreach my $dir (@${dirlist_aref}){
		print "$dir\n";
	}

	# find all domains and their directories
	my $alllist_aref = $vchkpw->alldomains(field => 'map');
	foreach my $href (@${alllist_aref}){
		print "$href->{name} => $href->{dir}\n";
	}

	my $domain = shift;
	unless(defined($domain)){
		print "enter domain: ";
		chop($domain=<STDIN>);
	}


	# find all mailboxes in a given domain
	my $mailboxes_aref = $vchkpw->domaininfo(domain => $domain, field => 'mailboxes');
	foreach my $mailbox (@{$mailboxes_aref}){
		print "found mailbox: $mailbox for domain: $domain\n";
	}

	# find all properties for a given domain
	my $alldata_aref = $vchkpw->domaininfo(domain => $domain, field => 'all');
	foreach my $href (@{$alldata_aref}){
		print "found data for $domain:\n";
		while(my($key,$value) = each %{$href}){
			print " found $key => $value\n";
		}
	}

	# individual user stuff
	my $email = shift;
	unless(defined($email)){
		print "email address: ";
		chop($email=<STDIN>);
	}

	my $dir = $vchkpw->userinfo(email => $email, field => 'dir');
	print "dir: $dir\n";
	my ($crypt,$uid,$gid) = $vchkpw->userinfo(email => $email, field => 'crypt,uid,gid');
	print "crypt/uid/gid: $crypt/$uid/$gid\n";
	my $comment = $vchkpw->userinfo(email => $email, field => 'comment');
	print "comment: $comment\n";
	my $maildir = $vchkpw->userinfo(email => $email, field => 'maildir');
	print "maildir: $maildir\n";
	my $quota = $vchkpw->userinfo(email => $email, field => 'quota');
	print "quota: $quota\n";
	my $plain = $vchkpw->userinfo(email => $email, field => 'plain');
	print "plain: $plain\n";

=head1 CAVEATS

This version is the first that supports SQL auth modules.  It is not
tested and should be used with caution.  Feedback needed.


=head1 AUTHOR

Jeremy Kister - http://jeremy.kister.net/

=cut

package Mail::vpopmail;

use strict;

my $HAVE_DBI;
eval{ require DBI; $HAVE_DBI=1; };

my (%_cache,%_arg);

sub new {
	my $class = shift;
	%_arg = @_;

	$_arg{cache} = 1 unless(defined($_arg{cache}));
	$_arg{debug} = 1 unless(defined($_arg{debug}));
	
	my $vpopdir = (getpwnam('vpopmail'))[7];
	die "vpopmail home directory ($vpopdir) not found.\n" unless(-d $vpopdir);

	if(open(MYSQL, "${vpopdir}/etc/vpopmail.mysql")){
		chop(my $input=<MYSQL>);
		my ($hostname,$dbport,$dbun,$dbpw,$dbname) = split(/\|/, $input);
		close MYSQL;

		my $dsn = "DBI:mysql:hostname=${hostname};database=${dbname}";
		$dsn .= ";port=$dbport" if($dbport);
		$_arg{dsn} = $dsn;
		$_arg{dbname} = $dbname;
		$_arg{dbun} = $dbun;
		$_arg{dbpw} = $dbpw;
		$_arg{auth_module} = 'sql';
	}elsif($_arg{auth_module} eq 'sql'){
		$_arg{dsn} = 'DBI:ldap:host=localhost;database=vpopmail' unless(defined($_arg{dsn}));
		($_arg{dbname}) = $_arg{dsn} =~ /database=([^\=\;\:\s]+)/;
		$_arg{dbun} = 'vpopmailuser' unless(defined($_arg{dbun}));
		$_arg{dbpw} = 'vpoppasswd' unless(defined($_arg{dbpw}));
	}else{
		$_arg{auth_module} = 'cdb';
	}
	
	if($_arg{auth_module} eq 'sql'){
		unless($HAVE_DBI){
			warn "You're trying to use SQL support, but do not have DBI in \@INC.  (\@INC contains: )";
			foreach(@INC){
				print "$_ ";
			}
			die "\nnew() failed-- ";
		}
	}
		
	return(bless({},$class));
}

sub Version { $Mail::vpopmail::VERSION }

sub _handle_dbh {
	my $dbh = ($_cache{dbh}) ? $_cache{dbh} : DBI->connect($_arg{dsn}, $_arg{dbun}, $_arg{dbpw}, {RaiseError => 1});

	unless($dbh){
		die "Connect to database failed: $DBI::errstr ";
	}
	if($_arg{cache}){
		$_cache{dbh} = $dbh unless($_cache{dbh});
	}
	return($dbh);
}

sub _dir {
	my $class = shift;
	if(my $domain = shift){
		return($_cache{$domain}{dir}) if($_cache{$domain}{dir});

		# assign is still authoritative when sql in use
		if(open(ASSIGN, '/var/qmail/users/assign')){
			my $dir;
			while(<ASSIGN>){
				if(/^\+${domain}\-:[^:]+:\d+:\d+:([^:]+):-:/){
					$dir = $1;
					last;
				}
			}
			close ASSIGN;

			if(defined($dir)){
				$_cache{$domain}{dir} = $dir if($_arg{cache});
				return($dir); # this dir is not verified, it's just what vpopmail thinks
			}else{
				warn "could not find directory for domain: $domain\n" if($_arg{debug});
			}
		}else{
			warn "could not open /var/qmail/users/assign: $!\n" if($_arg{debug});
		}
	}else{
		warn "domain not supplied correctly\n" if($_arg{debug});
	}
	return();
}

sub userinfo {
	my $class = shift;
	my %arg = @_;
	unless(exists($arg{email}) && exists($arg{field})){
		if($_arg{debug}){
			warn "syntax error: email: $arg{email} field: $arg{field}\n";
		}
		return();
	}
	my ($user,$domain) = split(/\@/, $arg{email}); # no routing data supported
	warn "arg{email}: $arg{email} - user: $user - domain: $domain\n" if($_arg{debug});

	if(defined($user) && defined($domain)){
		my @return;
		my $dir = Mail::vpopmail->_dir($domain);

		if($arg{field} eq 'dir'){
			push @return, $dir;
		}else{
			if(exists($_cache{$arg{email}}{crypt})){
			warn "cache found for $arg{email}\n" if($_arg{debug});
				foreach my $field (split(/,/, $arg{field})){
					push @return, $_cache{$arg{email}}{$field};
				}
			}else{
				my (%uhash,$found);
				if($_arg{auth_module} eq 'cdb'){
					if(open(VPASSWD, "${dir}/vpasswd")){
						while(<VPASSWD>){
							chomp;
							if(/^${user}:([^:]+):(\d+):(\d+):([^:]*):([^:]+):([^:]+)(:([^:]+))?/){
								%uhash = (crypt => $1, uid => $2, gid => $3, comment => $4,
								          maildir => $5, quota => $6, plain => $8, dir => $dir);
								$found=1;
								last;
							}
						}
						close VPASSWD;
					}else{
						warn "cannot open ${dir}/vpasswd: $!\n" if($_arg{debug});
					}
				}else{
					# sql
					my $dbh = _handle_dbh();
					my $sql = "SELECT pw_passwd,pw_uid,pw_gid,pw_gecos,pw_dir,pw_shell,pw_clear_passwd FROM $_arg{dbname}";
					$sql .= ' WHERE pw_name = ' . $dbh->quote($user) . ' AND pw_domain = ' . $dbh->quote($domain);
					my $sth = $dbh->prepare($sql);
					$sth->execute;
					my $row = $sth->fetchrow_arrayref;
					%uhash = (crypt => $row->[0], uid => $row->[1], gid => $row->[2], comment => $row->[3],
					          maildir => $row->[4], quota => $row->[5], plain => $row->[6], dir => ${dir});
					$found=1 if(exists($uhash{crypt}));
				}
				if($found){
					if($_arg{cache}){
						while(my($key,$value) = each %uhash){
							$_cache{$arg{email}}{$key} = $value;
						}
					}
	
					foreach my $field (split(/,/, $arg{field})){
						push @return, $uhash{$field};
					}
				}else{
					warn "cannot find ${user} in ${domain}\n" if($_arg{debug});
				}
			}
		}
		return (@return == 1) ? $return[0] : @return;
	}else{
		warn "email not supplied correctly\n" if($_arg{'debug'});
	}
	return();
}

sub alldomains {
	my $class = shift;
	my %arg = @_;
	unless($arg{field} eq 'name' || $arg{field} eq 'dir' || $arg{field} eq 'map'){
		if($_arg{debug}){
			warn "syntax error: field: $arg{field}\n";
		}
		return();
	}

	# assign is still authoritative when sql in use
	if(open(ASSIGN, '/var/qmail/users/assign')){
		my @array;
		while(<ASSIGN>){
			if(/^\+([^:]+)\-:[^:]+:\d+:\d+:([^:]+):-:/){
				if($arg{field} eq 'map'){
					push @array, { name => $1, dir => $2 };
				}elsif($arg{field} eq 'dir'){
					push @array, $2;
				}else{
					push @array, $1;
				}
			}
		}
		close ASSIGN;
		return(\@array);
	}else{	
		warn "could not open /var/qmail/users/assign: $!\n" if($_arg{debug});
	}
	return();
}

sub domaininfo {
	my $class = shift;
	my %arg = @_;

	if(exists($arg{domain}) && exists($arg{field})){
		unless($arg{field} eq 'mailboxes' || $arg{field} eq 'all' || $arg{field} eq 'dir'){
			warn "syntax error: domain field type may be 'mailboxes' or 'all'\n" if($_arg{debug});
			return();
		}
	}else{
		if($_arg{debug}){
			warn "syntax error: domain: $arg{domain} - field: $arg{field}\n";
		}
		return();
	}

	my %hash = ( dir => (exists($_cache{$arg{domain}}{dir})) ? $_cache{$arg{domain}}{dir} : Mail::vpopmail->_dir($arg{domain}) );
	warn "hash{dir}: $hash{dir}\n" if($_arg{debug});

	if($arg{field} eq 'dir'){
		return($hash{dir});
	}

	my @return;
	if($_arg{auth_module} eq 'cdb'){
		if(open(VPASSWD, "$hash{dir}/vpasswd")){
			while(<VPASSWD>){
				chomp;
				if(/^([^:]+):([^:]+):(\d+):(\d+):([^:]*):([^:]+):([^:]+)(:([^:]+))?/){
					%hash = (mailbox => $1, crypt => $2, uid => $3, gid => $4,
					         comment => $5, maildir => $6, quota => $7, plain => $9, dir => $hash{dir});
	
					if($arg{field} eq 'mailboxes'){
						push @return, $hash{mailbox};
					}else{
						push @return, \%hash;
					}
	
					if($_arg{cache}){
						while(my($key,$value) = each %hash){
							$_cache{$hash{mailbox}}{$key} = $value;
						}
					}
				}
			}
			close VPASSWD;
	
		}else{
			warn "cannot open $hash{dir}/vpasswd: $!\n" if($_arg{debug});
		}
	}else{
		#sql;
		my $dbh = _handle_dbh();
		my $sql = 'SELECT pw_name';
		$sql .= ',pw_passwd,pw_uid,pw_gid,pw_gecos,pw_dir,pw_shell,pw_clear_passwd' if($arg{field} eq 'all');
		$sql .= " FROM $_arg{dbname} WHERE pw_domain = " . $dbh->quote($arg{domain});
		my $sth = $dbh->prepare($sql);
		$sth->execute;
		while(my $row = $sth->fetchrow_arrayref){
			if($arg{field} eq 'mailboxes'){
				push @return, $row->[0];
			}else{
				push @return, { mailbox => $row->[0], crypt => $row->[1], uid => $row->[2], gid => $row->[3],
				                comment => $row->[4], maildir => $row->[5], quota => $row->[6],
				                plain => $row->[7], dir => $hash{dir} };
			}
		}
	}
	return(\@return);
}

1;
