#!/usr/bin/perl
#Copyright (c) 2007, Zane C. Bowers
#All rights reserved.
#
#Redistribution and use in source and binary forms, with or without modification,
#are permitted provided that the following conditions are met:
#
#   * Redistributions of source code must retain the above copyright notice,
#    this list of conditions and the following disclaimer.
#   * Redistributions in binary form must reproduce the above copyright notice,
#    this list of conditions and the following disclaimer in the documentation
#    and/or other materials provided with the distribution.
#
#THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
#ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 
#WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
#IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
#INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 
#BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 
#DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
#LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
#OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
#THE POSSIBILITY OF SUCH DAMAGE.


use IO::Select;
use IO::Socket;
use strict;
use warnings;
use Net::Server::Mail::ESMTP;
use Authen::Simple::PAM;
use Net::LDAP;
use Getopt::Std;
use Config::Tiny;
use Net::SMTP_auth;
use warnings;
use forks;

#print version
sub main::VERSION_MESSAGE {
	print "zms 1.0.0\n";
};

###############################################################################
#copyright info for &mwcqbinder
#
#old code writen while I was working at Midwest Connections Inc.
#
#Copyright (c) 2006, Midwest Connections Inc.
#All rights reserved.
#
#Redistribution and use in source and binary forms, with or without 
#modification, are permitted provided that the following conditions are met:
#
#    * Redistributions of source code must retain the above copyright notice,
#		this list of conditions and the following disclaimer.
#    * Redistributions in binary form must reproduce the above copyright notice,
#		this list of conditions and the following disclaimer in the documentation
#		and/or other materials provided with the distribution.
#    * Neither the name of the Midwest Connections Inc. nor the names of its
#		contributors may be used to endorse or promote products derived from
#		this software without specific prior written permission.
#
#THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
#AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
#IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
#ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
#FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
#DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
#SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
#CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
#OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
#THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

#quickly binds to a LDAP server and returns it.
sub mwcqbinder{
	my $binder=$_[0];
	my $password=$_[1];
	my $server=$_[2];
	my $port=$_[3];
	my $errorMethode=$_[4];#0 continue and return error
						   #1 exit with error and be verbose
						   #2 exit with error and be quiet

	#sets the port if it is not defined
	if (!defined($port)){
		$port="389";
	};
	
	#sets the $errorMethode if it is not defined
	if (!defined($errorMethode)){
		$errorMethode="1";
	};
	
	my %ldapconnection=();
	
	#connect to the ldap server
	$ldapconnection{ldap} = Net::LDAP->new( $server ) or $ldapconnection{status}=0;
	#checks if the status is equal to 0.
	if (defined($ldapconnection{status}) && $ldapconnection{status} == 0){
		if ($errorMethode == 0){
			$ldapconnection{LONGerror}="Could not contact the server.";
###############################################################################
#old code commented out... this is not how this should return an error...
#			return 1;
#end of old code
###############################################################################
#my new code... not connected to Midwest Connectins Inc.
			$ldapconnection{status}=0;
			return %ldapconnection;
#end of new code
###############################################################################
		};
		if ($errorMethode == 1){
			print "Could not contact the server.\n".
				"server: ".$server."\nport: ".$port."\n";			
			exit 1;
		};
		if ($errorMethode == 2){		
			exit 1;
		};
	};

	$ldapconnection{bindMesg} = $ldapconnection{ldap}->bind( $binder, password=>$password, version=>3 );
	if ($ldapconnection{bindMesg}->code) { #check for a failed bind
		if ($errorMethode == 0){
			$ldapconnection{status}=0;
			$ldapconnection{LONGerror}="Could not bind to the server.";
			return %ldapconnection;
		};
		if ($errorMethode == 1){
			print "Could not bind to the server.\nserver: ".$server."\nport: ".$port.
			"\nbinder: ".$binder."\n";
			exit 1;
		};
		if ($errorMethode == 2){
			exit 1;
		};
	}
	
	$ldapconnection{status}=1;
	return %ldapconnection
};
###############################################################################
#end copy from old code writen when I was working at Midwest Connections Inc.
###############################################################################

#print help
sub main::HELP_MESSAGE {
	&main::VERSION_MESSAGE;
	print "\n".
		"-c <file>   config file to use\n".
		"-h   print this\n";
	
	exit 1;
};

#declared here to prevent errors with &validate_recpipient
my @local_domains;

#validates a recipient
sub validate_recipient{
	my($session, $recipient) = @_;

	my $domain;
	if($recipient =~ /@(.*)>\s*$/){
		$domain = $1;
	}

	if(not defined $domain){
		return(0, 513, 'Syntax error.');
	}elsif(not(grep $domain eq $_, @local_domains)){
		#returns false if user is not authenticated.
		if(!defined($session->{AUTH}->{username})){
			return(0, 554, "$recipient: Recipient address rejected: Relay access denied");
		};
	}
		return(1);
}

#checks auth
sub validate_auth{
	my ($session, $username, $password) = @_;

	my $pam = Authen::Simple::PAM->new(service=>"smtp");

	if ($pam->authenticate( $username, $password)) {
		return 1;
	}else{
		return 0;
	};

}

sub get_outgoing_server_info{
	my ($session) = @_;
	
	#the hash that will hold the info returned
	my %outgoing_server_info;
	
	#set the status to error
	$outgoing_server_info{status}="0";
	
	#the user that this will bind ass
	my $LDAPuser="uid=".$session->{AUTH}->{username}.",".$session->{zmsConfig}->{userBaseDN};
	
	#the base DN that zmsAccount enteries will be looked for in
	my $baseDN="ou=zms,ou=.config,ou=".$session->{AUTH}->{username}.",".$session->{zmsConfig}->{homeOU};
	
	#binds to the server... do not exit on error here
	my %ldapconnection=mwcqbinder($LDAPuser, $session->{AUTH}->{password}, $session->{zmsConfig}->{LDAPserver},
		$session->{zmsConfig}->{LDAPport}, "0");
	
	#return if a connection could not be established
	if ($ldapconnection{status}=="0"){
		$outgoing_server_info{error}="could not bind to server to look up out going server info";
		return %outgoing_server_info;
	}
	
	my $mesg = $ldapconnection{ldap}->search(scope=>"sub",base=>$baseDN,
		filter=>"(objectClass=zmsAccount)");
	
	#makes the hash of the returned results
	my %LDAPhash=zLDAPhash($mesg);
	
	#get a list of found DNs
	my @DNs=keys(%LDAPhash);
	
	#get the sender address
	my $sender = $session->get_sender();
	
	#removes < and any thing before it
	$sender =~ s/.*<//;
	
	#removes > and any thing after it
	$sender =~ s/>.*//;
	
	#a array of attributes to migrate
	my @migrate=("zmsServerPassword", "zmsServerPort", "zmsServerAuthMethode",
		"zmsServerUsername", "zmsServerTimeout");
	
	my $DNs_int="0";
	while(defined($DNs[$DNs_int])){
		my $zmsFromRegexp_int="0";
		while(defined($LDAPhash{$DNs[$DNs_int]}{ldap}{zmsFromRegexp}[$zmsFromRegexp_int])){
			my $test=$sender;
			
			$test =~ s/$LDAPhash{$DNs[$DNs_int]}{ldap}{zmsFromRegexp}[$zmsFromRegexp_int]//;

			#if the regexp removes the entire sender, use this entry
			if ($test eq ""){

				#only one that needs defined
				if (defined($LDAPhash{$DNs[$DNs_int]}{ldap}{zmsServerHostname}[0])){
					$outgoing_server_info{zmsServerHostname}=$LDAPhash{$DNs[$DNs_int]}{ldap}{zmsServerHostname}[0];
				}else{
					$outgoing_server_info{error}="No server found for this account.";
					return %outgoing_server_info;
				};

				#migrate all found attributes
				my $migrate_int="0";
				while(defined($migrate[$migrate_int])){
					if (defined($LDAPhash{$DNs[$DNs_int]}{ldap}{$migrate[$migrate_int]}[0])){
						$outgoing_server_info{$migrate[$migrate_int]}=
							$LDAPhash{$DNs[$DNs_int]}{ldap}{$migrate[$migrate_int]}[0];
					};
					
					$migrate_int++;
				};
				
				#sets the server port if not given
				if(!defined($outgoing_server_info{zmsServerPort})){
					$outgoing_server_info{zmsServerPort}="25";
				};
				
				#sets the server timeout if not given
				#it is set low given this is not currently forking
				if(!defined($outgoing_server_info{zmsServerTimeoout})){
					$outgoing_server_info{zmsServerTimeout}="10";
				};
				
				#if no AUTH methode is listed, default to PLAIN
				if(!defined($outgoing_server_info{zmsServerAuthMethode})){
					$outgoing_server_info{zmsServerAuthMethode}="PLAIN";
				};
				
				#sets the status to success 
				$outgoing_server_info{status}="1";
				return %outgoing_server_info;
				
			};
			
			$zmsFromRegexp_int++;
		};

		$DNs_int++;
	};
	
	#if we are here, it means that it did not find it a matching out going account
	$outgoing_server_info{error}="No matching from addresses found.";
	return %outgoing_server_info;
};

sub queue_message{
	my($session, $data) = @_;

	#gets the sender
	my $sender = $session->get_sender();
	my @recipients = $session->get_recipients();

	return(0, 554, 'Error: no valid recipients')unless(@recipients);

	#refuse the data unless a user is authenticated...
	if ($session->{AUTH}->{username} eq ""){
		return(0, 554, 'Error: authentication failed... refusing DATA');
	};

	my %outgoing_server_info=&get_outgoing_server_info($session);

	if ($outgoing_server_info{status} == "0"){
		return(0, 554, 'Error: '.$outgoing_server_info{error});
	};

	my $smtp = Net::SMTP_auth->new($outgoing_server_info{zmsServerHostname}.":".
		$outgoing_server_info{zmsServerPort},
		Timeout => $outgoing_server_info{zmsServerTimeout}
		);
	if (!$smtp){
		return(0, 554, "Error: could not connect to server, ".
			$outgoing_server_info{zmsServerHostname}.".");
	};

	if (!$smtp->auth($outgoing_server_info{zmsServerAuthMethode},
			$outgoing_server_info{zmsServerUsername},
			$outgoing_server_info{zmsServerPassword}
			)){
				return(0, 554, 'Error: could not auth with out going server, '.
					$outgoing_server_info{zmsServerHostname}.".");
			};

	#sends the sender info to the out going server
	if(!$smtp->mail($sender)){
		return(0, 554, 'Error: sending sender, '.$sender.',with out going server, '.
			$outgoing_server_info{zmsServerHostname}.".");
	};

	#sends a list of recipients 
	if(!$smtp->recipient(@recipients)){
		return(0, 554, 'Error: sending recipients to out going server');
	};

	#initialize, send DATA, end sending DATA
	if (!$smtp->data()){
		return(0, 554, 'Error: initializing DATA session');
	};
	if (!$smtp->datasend($$data)){
		return(0, 554, 'Error: sending DATA');
	};
	if(!$smtp->dataend()){
		return(0, 554, 'Error: ending DATA session');
	};

	#quits the SMTP session
	$smtp->quit;

	return(1, 250, "message queued");
}

#makes a hash out of a returned LDAP search
sub  zLDAPhash {
	my $mesg = $_[0]; #the object returned from a LDAP search

	#used for holding the data, before returning it
	my %data;

	#builds it
	my $entryinter=0;
	my $max = $mesg->count;
	for ( $entryinter = 0 ; $entryinter < $max ; $entryinter++ ){
		my $entry = $mesg->entry ( $entryinter );
		$data{$entry->dn}={ldap=>{dn=>$entry->dn},internal=>{changed=>0}};
		#builds a hash of attributes
		foreach my $attr ( $entry->attributes ) {
			$data{$entry->dn}{ldap}{$attr}=[];

			#builds the array of values for the attribute
			my $valueinter=0;
			my @attributes=$entry->get_value($attr);
			while (defined($attributes[$valueinter])){
				$data{$entry->dn}{ldap}{$attr}[$valueinter]=$attributes[$valueinter];
				$valueinter++;
			};
        };
	};

	return %data;
};

sub generateConfig{
	my $config_file=$_[0];
	
	my %config;

	#puts file back in
	$config{file}=$config_file;

	my $ini = Config::Tiny->new();

	#reads the config file	
	$ini = Config::Tiny->read($config_file);
	
	$config{LDAPport}="389";
	
	#autoconfig if baseDN is given... these will be redefined later if asked to...
	if(defined($ini->{_}->{baseDN})){
		$config{baseDN}=$ini->{_}->{baseDN};
		$config{userDN}="uid=".$ENV{USER}.",ou=users,".$ini->{_}->{baseDN};
		$config{userBaseDN}="ou=users,".$ini->{_}->{baseDN};
		$config{homeOU}="ou=home,".$ini->{_}->{baseDN};
		$config{serverOU}="ou=zms,ou=.config,ou=,".$ENV{USER}.",ou=home,".$ini->{_}->{baseDN};
	};
	
	#values to migrate if defined... over ride auto defined from baseDN
	my @migrate=("userDN", "homeOU", "userBaseDN","serverOU", "password", "LDAPserver", "LDAPport");
	
	my $int=0; #used for running through migrate
	
	#migrate the values
	while(defined($migrate[$int])){
		if (defined($ini->{_}->{$migrate[$int]})){
			$config{$migrate[$int]}=$ini->{_}->{$migrate[$int]};
		};
		$int++;
	};
	
	if(!defined($config{password})){
		print "No password for connecting to the LDAP server. Anonmous binds not supported for security reasons.\n";
		exit 1;
	}
	
	return %config;
};

sub get_local_domains{
	my %config = @_;
	
	my %local_domains_hash;

	#connect to the LDAP server or exit and issue error
	my %ldapconnection=mwcqbinder($config{userDN}, $config{password}, $config{LDAPserver}, $config{LDAPport});

	my $mesg = $ldapconnection{ldap}->search(scope=>"sub",base=>$config{serverOU},filter=>"(objectClass=zmsServer)");
	
	#makes the hash of the returned results
	my %LDAPhash=zLDAPhash($mesg);
	
	$mesg=$ldapconnection{ldap}->unbind;
	
	#get a list of found DNs
	my @DNs=keys(%LDAPhash);

	#runs through each DN adding every zmsDomain found in each DN to %local_domains_hash
	my $DNs_int="0";
	while(defined($DNs[$DNs_int])){
		my $zmsDomain_int="0";
		while(defined($LDAPhash{$DNs[$DNs_int]}{ldap}{zmsDomain}[$zmsDomain_int])){
			$local_domains_hash{$LDAPhash{$DNs[$DNs_int]}{ldap}{zmsDomain}[$zmsDomain_int]}=
				$LDAPhash{$DNs[$DNs_int]}{ldap}{zmsDomain}[$zmsDomain_int];
			
			$zmsDomain_int++;
		};
		
		$DNs_int++;
	};
	
	#converts the domains hash to a array
	my @local_domains_array=keys(%local_domains_hash);

	return @local_domains_array;
};

sub connection_fork{
	my $conn=$_[0];
	#my $config=[1];
	my $esmtp=$_[1];

	
	#my $esmtp = new Net::Server::Mail::ESMTP(socket => $conn);
	#$esmtp->register('Net::Server::Mail::ESMTP::AUTH');
	#$esmtp->register('Net::Server::Mail::ESMTP::8BITMIME');
	#$esmtp->register('Net::Server::Mail::ESMTP::PIPELINING');
	#$esmtp->{zmsConfig}={$config};
	#$esmtp->set_callback(AUTH => \&validate_auth);
	#$esmtp->set_callback(RCPT => \&validate_recipient);
	#$esmtp->set_callback(DATA => \&queue_message);
	$esmtp->process();
	
	$conn->close();

	return "worked";
};

my %config;

my %opts;
getopts("c:hf", \%opts);

#get which config file to use
if(!defined($opts{c})){
	$config{file}="/usr/local/etc/zms.conf"
}else{
	$config{file}=$opts{c};
};

if (defined($opts{h})){
	&main::HELP_MESSAGE;
};

#exits if the file does not exist
if (! -f $config{file}){
	print $config{file}." does not exist\n";
	exit 1;
}else{
	%config=generateConfig($config{file});
};

#enables forking if needed
if (defined($opts{f})){
	$config{forking}="on";
};

#get the local domains that the server handles mail for
@local_domains=&get_local_domains(%config);

my $server = new IO::Socket::INET(Listen => 1, LocalPort => 2525);


while(my $conn = $server->accept){

	my $esmtp = new Net::Server::Mail::ESMTP(socket => $conn);
	$esmtp->register('Net::Server::Mail::ESMTP::AUTH');
	$esmtp->register('Net::Server::Mail::ESMTP::8BITMIME');
	$esmtp->register('Net::Server::Mail::ESMTP::PIPELINING');
	$esmtp->{zmsConfig}={%config};
	$esmtp->set_callback(AUTH => \&validate_auth);
	$esmtp->set_callback(RCPT => \&validate_recipient);
	$esmtp->set_callback(DATA => \&queue_message);

	if (defined($config{forking})){ 
		my $thread = threads->new( \&connection_fork, $conn, $esmtp);
		$thread->detach;
	}else{
		$esmtp->process();
	
		$conn->close();
	};

};

#should never get here...
exit 1;

#-----------------------------------------------------------
# POD documentation section
#-----------------------------------------------------------
=pod

=head1 NAME

zms - A specialized mail gateway system for using user specified SMTP server.

=head1 SYNOPSIS

zms [B<-c> <config file>] [B<-f>]

=head1 FLAGS

=item -c <config file>

The config file to use. Defaults to /usr/local/etc/zms.conf.

=item -f

Enables forking on new messages so it can process more than one. NOTE: THIS
DOES NOT CURRENTLY WORK! It will exit after the first mail is sent for some
unknown reason.

=head1 CONFIG FILE

The default is /usr/local/etc/zmc.conf. It is in VARIABLE=VALUE format.

=item LDAPserver

This is the LDAP server to use.

=item LDAPport

Which port to try to connect to for accessing the LDAP server.

=item password

The password to bind to the server file.

=item baseDN

The base DN used for autoconf if desired.

=item userDN

The DN to do a simple bind as for connecting up to the LDAP server for the
server to use. Defaults to uid=$ENV{USER},ou=users,$baseDN.

=item serverOU

The OU that contains zmsServer enteries under it. Defaults to
ou=zms,ou=.config,ou=$ENV{USER},ou=home,$baseDN.

=item userBaseDN

The base DN to try for using when creating the string for the user bind. The
default is ou=users,$baseDN. When a user authenticates and sends the message,
zms will try to bind as that user. It will use uid=$user,$userBaseDN.

=item homeOU

This is the OU that contains users home OUs. Defaults to ou=home,$baseDN. zms will
check in ou=zms,ou=.config,ou=$user,$homeOU for zmsAccount enteries.

=head1 ZMSACCOUNT

A LDAP entry of the objectClass zmsAccount may have cn, zmsServerHostname,
zmsserverPort, zmsServerUsername, zmsServerPassword, zmsServerAuthMethode,
and zmsFromRegexp.

Unless otherwise specified, only the first attribute of each type is used.

Sending through a out going server with out SMTP authentication is not currently
supported.

=item cn

This is just used for assigning a name to the entry.

=item zmsServerHostname

This is the hostname for the out going server.

=item zmsServerPort

This is the port to connect to on the out going server. The defualt
is 25.

=item zmsServerUsername

This is the username to use for when authenticating to the server.

=item zmsServerPassword

The password to use when authenticating to the remote server.

=item zmsServerAuthMethode

The methode to use when authenticating. The default is PLAIN.

=item zmsFromRegexp

This is the regexp used for checking if out going server this entry is
for should be used. There may be more than one of these attributes in
a entry.

=head1 EXAMPLE ZMSACCOUNT ENTRY

	dn: cn=moose@foo.bar,ou=zms,ou=.config,ou=toad,ou=home,dc=bufo
	objectClass: zmsAccount
	cn: toad
	zmsServerHostname: mail.foo.bar
	zmsServerPort: 25
	zmsServerUsername: moose
	zmsServerPassord: moosetime
	zmsServerAuthMethode: LOGIN
	zmsFromRegexp: ^moose@foo.bar$

=head1 ZMSSERVER

The objectClass zmsServer may have the attributes cn, zmsDomain, zmsSpoolDir,
and zmsServerPort. Only zmsDomain is currently required. This is the servers
local domain.

=item cn

Used for naming a entry.

=item zmsDomain

This is a local domain for the email server. Not currently used, but is needed.
Generally this should be the hostname of the machine zms is running on currently.

=item zmsSpooldir

This is the dir used for spoolings. Currently zms just sends it before reporting
it as a success to the client.

=item zmsServerPort

This is the port that the server should run on. Will be implemented in the next
beta release.

dn: cn=bufo,ou=zms,ou=.config,ou=smtpd,ou=home,dc=bufo
cn: bufo
zmsDomain: bufo
zmsServerPort: 25

=head1 HOMEOU INFO

Thome homeOU revolves around the idea of having a homeOU for LDAP users. This allows
them to store data in it in a manner.

	access to dn.regex="^(.+,)?ou=([^,]+),ou=home,dc=bufo$"
   		by dn.exact,expand="uid=$2,ou=users,dc=bufo" write
   		by * none

=head1 LDAP SCHEMA

zms.schema may be found at http://vvelox.net/src/ldap/zms.schema

=head1 VERSION

1.0.0

=head1 TODO

=item Implement more sanity checking.
=item Figure out how to better describe userDN.
=item Fix forking.
=item Implement TLS for out going.
=item Move to being a full fledge email server.
=item Implement spooling.

=head1 AUTHOR

Zane C. Bowers <vvelox@vvelox.net>

=head1 COPYRIGHT

Copyright (c) 2007, Zame C. Bowers <vvelox@vvelox.net>

All rights reserved.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:

    * Redistributions of source code must retain the above copyright notice,
     this list of conditions and the following disclaimer.
    * Redistributions in binary form must reproduce the above copyright
     notice, this list of conditions and the following disclaimer in the
     documentation and/or other materials provided with the distribution.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS` OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

=head1 SCRIPT CATEGORIES

Mail

=head1 OSNAMES

unix

=head1 README

zms - A specialized mail system for using user specified SMTP server.

=cut
#-----------------------------------------------------------
# End of POD documentation
#-----------------------------------------------------------