From 170f4973aacae14f3e6fe90d606521ff45c58cfd Mon Sep 17 00:00:00 2001 From: lars Date: Sun, 12 Oct 2008 01:24:18 +0000 Subject: [PATCH] module Mail::Ezmlm: * tagged v0.08 * added source and debian package --- Ezmlm/tags/Ezmlm-0.08/Changes | 58 + Ezmlm/tags/Ezmlm-0.08/Ezmlm.pm | 1234 +++++++++++++++++ Ezmlm/tags/Ezmlm-0.08/Ezmlm/GpgEzmlm.pm | 887 ++++++++++++ Ezmlm/tags/Ezmlm-0.08/Ezmlm/GpgKeyRing.pm | 399 ++++++ Ezmlm/tags/Ezmlm-0.08/MANIFEST | 9 + Ezmlm/tags/Ezmlm-0.08/META.yml | 10 + Ezmlm/tags/Ezmlm-0.08/Makefile.PL | 232 ++++ Ezmlm/tags/Ezmlm-0.08/README | 19 + Ezmlm/tags/Ezmlm-0.08/test.pl | 234 ++++ Ezmlm/tags/packages/Ezmlm-0.08.tar.gz | Bin 0 -> 25039 bytes .../libemail-ezmlm-perl_0.08-1_all.deb | Bin 0 -> 36828 bytes 11 files changed, 3082 insertions(+) create mode 100644 Ezmlm/tags/Ezmlm-0.08/Changes create mode 100644 Ezmlm/tags/Ezmlm-0.08/Ezmlm.pm create mode 100644 Ezmlm/tags/Ezmlm-0.08/Ezmlm/GpgEzmlm.pm create mode 100644 Ezmlm/tags/Ezmlm-0.08/Ezmlm/GpgKeyRing.pm create mode 100644 Ezmlm/tags/Ezmlm-0.08/MANIFEST create mode 100644 Ezmlm/tags/Ezmlm-0.08/META.yml create mode 100644 Ezmlm/tags/Ezmlm-0.08/Makefile.PL create mode 100644 Ezmlm/tags/Ezmlm-0.08/README create mode 100644 Ezmlm/tags/Ezmlm-0.08/test.pl create mode 100644 Ezmlm/tags/packages/Ezmlm-0.08.tar.gz create mode 100644 Ezmlm/tags/packages/libemail-ezmlm-perl_0.08-1_all.deb diff --git a/Ezmlm/tags/Ezmlm-0.08/Changes b/Ezmlm/tags/Ezmlm-0.08/Changes new file mode 100644 index 0000000..14b5212 --- /dev/null +++ b/Ezmlm/tags/Ezmlm-0.08/Changes @@ -0,0 +1,58 @@ +Revision history for Perl extension Mail::Ezmlm. + +0.01 Sun Oct 31 12:58:16 1999 + - original version; created by h2xs 1.1.1.1.2.1 + +0.02 Wed Jan 26 07:59:10 SAST 2000 + - Added functions to check various options + (ismodsub, ismodpost, isremote, isdeny, isallow, isdigest) + - Allowed sub, unsub, list, subscribers, issub to work with list subparts + (ie, the allow, deny, mod, digest sub directories) + - Changed system() calls to safer ones (ie command, switches) + - Made error handling better (errmsg() and errno()) + - Added support for creating MySQL tables via ezmlm-mktab + +0.03 Mon Sep 25 11:49:26 SAST 2000 + - fixed the issub() function + - fixed the problem with dashes in hostnames. + - hopefully got rid of some of the warnings from sub() and unsub() + +0.04 Mon May 26 18:15:38 SAST 2003 + - fixed return value of Makefile.PL (Andrew Pam ) + - fixed issub() (again) to handle parts properly (bug 602; moguo@servism.com) + - converted module global variables to instance variables + +0.05 Sat Mar 5 12:47:10 SAST 2005 + - fixed forced scalar return in subscribers() (Jon Coulter ) + - fixed handling of dashes in hostnames (bug 5571; Lars Braeuer ) + - fixed some tainting problems (Scott Beck and Matt Simerson ) + - fixed order of control/defaulthost and control/me (bug 1515) + - fixed a bug in Makefile.PL (bug 11771). does not affect most users, so released as 0.05.1 + +0.06 Mon Dec 26 18:55:12 CET 2005 + - support for ezmlm-idx-5.0.0 added + - fixed version check + +0.07 Mon Jan 2 22:12:32 CET 2006 + - new functions for text management (idx >= 5.0) + - new functions for language setting (idx >= 5.0) + - new functions for charset setting (idx >= 5.0) + - new functions for config directory setting (idx >= 5.0) + - look for ezmlm-make at run-time + - requires Text::ParseWords + +0.07.1 Mon Jan 23 22:30:14 CET 2006 + - fix misinterpretation of empty settings + +0.07.2 Sun May 6 06:20:13 CEST 2006 + - fix parsing of ezmlm-make options + +0.07.2 Tue Jun 20 01:05:56 UTC 2006 + - fixed 'get_charset' and 'set_charset' for idx < 5.0 + +0.08 Thu Oct 2 03:23:06 CEST 2008 + - fixed handling of the 'owner' setting for ezmlm-idx > v5 + - updated ezmlm-idx version detection + - allow "@" in the path of a mailing list + - add modules Mail::Ezmlm::GpgKeyRing and Mail::Ezmlm::GpgEzmlm + diff --git a/Ezmlm/tags/Ezmlm-0.08/Ezmlm.pm b/Ezmlm/tags/Ezmlm-0.08/Ezmlm.pm new file mode 100644 index 0000000..87a8259 --- /dev/null +++ b/Ezmlm/tags/Ezmlm-0.08/Ezmlm.pm @@ -0,0 +1,1234 @@ +# =========================================================================== +# Ezmlm.pm - version 0.08 - 10/12/2008 +# $Id$ +# +# Object methods for ezmlm mailing lists +# +# Copyright (C) 1999-2005, Guy Antony Halse, All Rights Reserved. +# Copyright (C) 2005-2008, Lars Kruse, All Rights Reserved. +# Please send bug reports and comments to ezmlm-web@sumpfralle.de. +# +# 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 name Guy Antony Halse nor the names of any 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 REGENTS 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. +# +# ========================================================================== +# POD is at the end of this file. Search for '=head' to find it +package Mail::Ezmlm; + +use strict; +use vars qw($QMAIL_BASE $EZMLM_BASE $MYSQL_BASE $VERSION @ISA @EXPORT @EXPORT_OK); +use Carp; +use Text::ParseWords; + +require Exporter; + +@ISA = qw(Exporter); +# Items to export into callers namespace by default. Note: do not export +# names by default without a very good reason. Use EXPORT_OK instead. +# Do not simply export all your public functions/methods/constants. +@EXPORT = qw( + +); +$VERSION = '0.08'; + +require 5.005; + +# == Begin site dependant variables == +$EZMLM_BASE = '/usr/local/bin'; #Autoinserted by Makefile.PL +$QMAIL_BASE = '/var/qmail'; #Autoinserted by Makefile.PL +$MYSQL_BASE = ''; #Autoinserted by Makefile.PL +# == End site dependant variables == + +# == check the ezmlm-make path == +$EZMLM_BASE = '/usr/local/bin/ezmlm' unless (-e "$EZMLM_BASE/ezmlm-make"); +$EZMLM_BASE = '/usr/local/bin/ezmlm-idx' unless (-e "$EZMLM_BASE/ezmlm-make"); +$EZMLM_BASE = '/usr/local/bin' unless (-e "$EZMLM_BASE/ezmlm-make"); +$EZMLM_BASE = '/usr/bin/ezmlm' unless (-e "$EZMLM_BASE/ezmlm-make"); +$EZMLM_BASE = '/usr/bin/ezmlm-idx' unless (-e "$EZMLM_BASE/ezmlm-make"); +$EZMLM_BASE = '/usr/bin' unless (-e "$EZMLM_BASE/ezmlm-make"); + +# == clean up the path for taint checking == +local $ENV{'PATH'} = $EZMLM_BASE; + +# == Initialiser - Returns a reference to the object == +sub new { + my($class, $list) = @_; + my $self = {}; + bless $self, ref $class || $class || 'Mail::Ezmlm'; + $self->setlist($list) if(defined($list) && $list); + return $self; +} + +# == Make a new mailing list and set it to current == +sub make { + my($self, %list) = @_; + my($VHOST, $comandline, $hostname); + + # Do we want to use command line switches + my $commandline = ''; + $commandline = '-' . $list{'-switches'} if(defined($list{'-switches'})); + my @commandline; + foreach ("ewords('\s+', 1, $commandline)) { + next if (!defined($_)); + # untaint input + $_ =~ s/['"]//g; + $_ =~ m/^([\w _\/,\.\@:'"-]*)$/; + if ($_ =~ /^\s*$/) { + push @commandline, ""; + } else { + push @commandline, $1; + } + } + + # These three variables are essential + ($self->_seterror(-1, 'must define -dir in a make()') && return 0) unless(defined($list{'-dir'})); + ($self->_seterror(-1, 'must define -qmail in a make()') && return 0) unless(defined($list{'-qmail'})); + ($self->_seterror(-1, 'must define -name in a make()') && return 0) unless(defined($list{'-name'})); + + # Determine hostname if it is not supplied + $hostname = $self->_getdefaultdomain; + if(defined($list{'-host'})) { + $VHOST = 1 unless ($list{'-host'} eq $hostname); + } else { + $list{'-host'} = $hostname; + } + + # does the mailing list directory already exist? + if (-e $list{'-dir'}) { + $self->_seterror(-1, + '-the mailing list directory already exists: ' . $list{'-dir'}); + return undef; + } + + # Attempt to make the list if we can. + if (system("$EZMLM_BASE/ezmlm-make", @commandline, $list{'-dir'}, $list{'-qmail'}, $list{'-name'}, $list{'-host'}) != 0) { + $self->_seterror($?, '-failed to create mailing list - check your webserver\'s log file for details'); + return undef; + } + + # Sort out the DIR/inlocal problem if necessary + if(defined($VHOST)) { + unless(defined($list{'-user'})) { + ($self->_seterror(-1, '-user must match virtual host user in make()') && return 0) unless($list{'-user'} = $self->_getvhostuser($list{'-host'})); + } + + open(INLOCAL, ">$list{'-dir'}/inlocal") || ($self->_seterror(-1, 'unable to read inlocal in make()') && return 0); + print INLOCAL $list{'-user'} . '-' . $list{'-name'} . "\n"; + close INLOCAL; + } + + $self->_seterror(undef); + return $self->setlist($list{'-dir'}); +} + +# == Update the current list == +sub update { + my($self, $switches) = @_; + my($outhost, $inlocal); + + # Do we have the command line switches + ($self->_seterror(-1, 'nothing to update()') && return 0) unless(defined($switches)); + $switches = '-e' . $switches; + my @switch_list; + + foreach ("ewords('\s+', 1, $switches)) { + next if (!defined($_)); + # untaint input + $_ =~ s/['"]//g; + $_ =~ m/^([\w _\/,\.\@:'"-]*)$/; + if ($_ =~ /^\s*$/) { + push @switch_list, ""; + } else { + push @switch_list, $1; + } + } + + # can we actually alter this list; + ($self->_seterror(-1, 'must setlist() before you update()') && return 0) unless(defined($self->{'LIST_NAME'})); + # check for important files: 'config' (idx < v5.0) or 'flags' (idx >= 5.0) + ($self->_seterror(-1, "$self->{'LIST_NAME'} does not appear to be a valid list in update()") && return 0) unless((-e "$self->{'LIST_NAME'}/config") || (-e "$self->{'LIST_NAME'}/flags")); + + # Work out if this is a vhost. + open(OUTHOST, "<$self->{'LIST_NAME'}/outhost") || ($self->_seterror(-1, 'unable to read outhost in update()') && return 0); + chomp($outhost = ); + close(OUTHOST); + + # Save the contents of inlocal if it is a vhost + unless($outhost eq $self->_getdefaultdomain) { + open(INLOCAL, "<$self->{'LIST_NAME'}/inlocal") || ($self->_seterror(-1, 'unable to read inlocal in update()') && return 0); + chomp($inlocal = ); + close(INLOCAL); + } + + # Attempt to update the list if we can. + system("$EZMLM_BASE/ezmlm-make", @switch_list, $self->{'LIST_NAME'}) == 0 + || ($self->_seterror($?) && return undef); + + # Sort out the DIR/inlocal problem if necessary + if(defined($inlocal)) { + open(INLOCAL, ">$self->{'LIST_NAME'}/inlocal") || ($self->_seterror(-1, 'unable to write inlocal in update()') && return 0); + print INLOCAL "$inlocal\n"; + close INLOCAL; + } + + $self->_seterror(undef); + return $self->{'LIST_NAME'}; +} + +# == Get a list of options for the current list == +sub getconfig { + my($self) = @_; + my($options); + + # Read the config file + if(-e $self->{LIST_NAME} . "/flags") { + # this file exists since ezmlm-idx-5.0.0 + # 'config' is not authorative anymore since that version + $options = $self->_getconfig_idx5(); + } elsif(open(CONFIG, "<" . $self->{LIST_NAME} . "/config")) { + # 'config' contains the authorative information + while() { + if (/^F:-(\w+)/) { + $options = $1; + } elsif (/^(\d):(.+)$/) { + my $opt_num = $1; + my $value = $2; + $options .= " -$opt_num '$value'" if ($value =~ /\S/); + } + } + close CONFIG; + } else { + # Try manually - this will ignore all string settings, that can only be found + # in the config file + $options = $self->_getconfigmanual(); + } + + ($self->_seterror(-1, 'unable to read configuration in getconfig()') && return undef) unless (defined($options)); + + $self->_seterror(undef); + return $options; +} + +# == Return the name of the current list == +sub thislist { + my($self) = shift; + $self->_seterror(undef); + return $self->{'LIST_NAME'}; +} + +# == Set the current mailing list == +sub setlist { + my($self, $list) = @_; + if ($list =~ m/^([\w\d\_\-\.\/\@]+)$/) { + $list = $1; + if (-e "$list/lock") { + $self->_seterror(undef); + return $self->{'LIST_NAME'} = $list; + } else { + $self->_seterror(-1, "$list does not appear to be a valid list in setlist()"); + return undef; + } + } else { + $self->_seterror(-1, "$list contains tainted data in setlist()"); + return undef; + } +} + +# == Output the subscribers to $stream == +sub list { + my($self, $stream, $part) = @_; + $stream = *STDOUT unless (defined($stream)); + if(defined($part)) { + print $stream $self->subscribers($part); + } else { + print $stream $self->subscribers; + } +} + +# == Return an array of subscribers == +sub subscribers { + my($self, $part) = @_; + my(@subscribers); + ($self->_seterror(-1, 'must setlist() before returning subscribers()') && return undef) unless(defined($self->{'LIST_NAME'})); + if(defined($part) && $part) { + ($self->_seterror(-1, "$part part of $self->{'LIST_NAME'} does not appear to exist in subscribers()") && return undef) unless(-e "$self->{'LIST_NAME'}/$part"); + @subscribers = map { s/[\r\n]// && $_ } sort `$EZMLM_BASE/ezmlm-list $self->{'LIST_NAME'}/$part`; + } else { + @subscribers = map { s/[\r\n]// && $_ } sort `$EZMLM_BASE/ezmlm-list $self->{'LIST_NAME'}`; + } + + if($?) { + $self->_seterror($?, 'error during ezmlm-list in subscribers()'); + return (scalar @subscribers ? @subscribers : undef); + } else { + $self->_seterror(undef); + return @subscribers; + } +} + +# == Subscribe users to the current list == +sub sub { + my($self, @addresses) = @_; + ($self->_seterror(-1, 'sub() must be called with at least one address') && return 0) unless @addresses; + my($part) = pop @addresses unless ($#addresses < 1 or $addresses[$#addresses] =~ /\@/); + my($address); + ($self->_seterror(-1, 'must setlist() before sub()') && return 0) unless(defined($self->{'LIST_NAME'})); + + if(defined($part) && $part) { + ($self->_seterror(-1, "$part of $self->{'LIST_NAME'} does not appear to exist in sub()") && return 0) unless(-e "$self->{'LIST_NAME'}/$part"); + foreach $address (@addresses) { + next unless $self->_checkaddress($address); + system("$EZMLM_BASE/ezmlm-sub", "$self->{'LIST_NAME'}/$part", $address) == 0 || + ($self->_seterror($?) && return undef); + } + } else { + foreach $address (@addresses) { + next unless $self->_checkaddress($address); + system("$EZMLM_BASE/ezmlm-sub", $self->{'LIST_NAME'}, $address) == 0 || + ($self->_seterror($?) && return undef); + } + } + $self->_seterror(undef); + return 1; +} + +# == Unsubscribe users from a list == +sub unsub { + my($self, @addresses) = @_; + ($self->_seterror(-1, 'unsub() must be called with at least one address') && return 0) unless @addresses; + my($part) = pop @addresses unless ($#addresses < 1 or $addresses[$#addresses] =~ /\@/); + my($address); + ($self->_seterror(-1, 'must setlist() before unsub()') && return 0) unless(defined($self->{'LIST_NAME'})); + + if(defined($part) && $part) { + ($self->_seterror(-1, "$part of $self->{'LIST_NAME'} does not appear to exist in unsub()") && return 0) unless(-e "$self->{'LIST_NAME'}/$part"); + foreach $address (@addresses) { + next unless $self->_checkaddress($address); + system("$EZMLM_BASE/ezmlm-unsub", "$self->{'LIST_NAME'}/$part", $address) == 0 || + ($self->_seterror($?) && return undef); + } + } else { + foreach $address (@addresses) { + next unless $self->_checkaddress($address); + system("$EZMLM_BASE/ezmlm-unsub", $self->{'LIST_NAME'}, $address) == 0 || + ($self->_seterror($?) && return undef); + } + } + $self->_seterror(undef); + return 1; +} + +# == Test whether people are subscribed to the list == +sub issub { + my($self, @addresses) = @_; + my($part) = pop @addresses unless ($#addresses < 1 or $addresses[$#addresses] =~ /\@/); + my($address, $issub); $issub = 1; + ($self->_seterror(-1, 'must setlist() before issub()') && return 0) unless(defined($self->{'LIST_NAME'})); + + local $ENV{'SENDER'}; + + if(defined($part) && $part) { + ($self->_seterror(-1, "$part of $self->{'LIST_NAME'} does not appear to exist in issub()") && return 0) unless(-e "$self->{'LIST_NAME'}/$part"); + foreach $address (@addresses) { + $ENV{'SENDER'} = $address; + undef($issub) if ((system("$EZMLM_BASE/ezmlm-issubn", "$self->{'LIST_NAME'}/$part") / 256) != 0) + } + } else { + foreach $address (@addresses) { + $ENV{'SENDER'} = $address; + undef($issub) if ((system("$EZMLM_BASE/ezmlm-issubn", $self->{'LIST_NAME'}) / 256) != 0) + } + } + + $self->_seterror(undef); + return $issub; +} + +# == Is the list posting moderated == +# DEPRECATED: useless - you should better check the appropriate config flag +sub ismodpost { + my($self) = @_; + ($self->_seterror(-1, 'must setlist() before ismodpost()') && return 0) unless(defined($self->{'LIST_NAME'})); + $self->_seterror(undef); + return -e "$self->{'LIST_NAME'}/modpost"; +} + +# == Is the list subscriber moderated == +# DEPRECATED: useless - you should better check the appropriate config flag +sub ismodsub { + my($self) = @_; + ($self->_seterror(-1, 'must setlist() before ismodsub()') && return 0) unless(defined($self->{'LIST_NAME'})); + $self->_seterror(undef); + return -e "$self->{'LIST_NAME'}/modsub"; +} + +# == Is the list remote adminable == +# DEPRECATED: useless - you should better check the appropriate config flag +sub isremote { + my($self) = @_; + ($self->_seterror(-1, 'must setlist() before isremote()') && return 0) unless(defined($self->{'LIST_NAME'})); + $self->_seterror(undef); + return -e "$self->{'LIST_NAME'}/remote"; +} + +# == Does the list have a kill list == +# DEPRECATED: useless - you should better check the appropriate config flag +sub isdeny { + my($self) = @_; + ($self->_seterror(-1, 'must setlist() before isdeny()') && return 0) unless(defined($self->{'LIST_NAME'})); + $self->_seterror(undef); + return -e "$self->{'LIST_NAME'}/deny"; +} + +# == Does the list have an allow list == +# DEPRECATED: useless - the allow list is always created automatically +sub isallow { + my($self) = @_; + ($self->_seterror(-1, 'must setlist() before isallow()') && return 0) unless(defined($self->{'LIST_NAME'})); + $self->_seterror(undef); + return -e "$self->{'LIST_NAME'}/allow"; +} + +# == Is this a digested list == +# DEPRECATED: useless - you should better check the appropriate config flag +sub isdigest { + my($self) = @_; + ($self->_seterror(-1, 'must setlist() before isdigest()') && return 0) unless(defined($self->{'LIST_NAME'})); + $self->_seterror(undef); + return -e "$self->{'LIST_NAME'}/digest"; +} + +# == retrieve file contents == +sub getpart { + my($self, $part) = @_; + my(@contents, $content); + # check for the file in the list directory first + my $filename = $self->{'LIST_NAME'} . "/$part"; + # check for default file in config directory, if necessary + # BEWARE: get_config_dir and get_lang may _not_ cause an eternal loop :) + $filename = $self->get_config_dir() . '/' . $self->get_lang() . "/$part" + if (!(-e "$filename") && (get_version() >= 5) && + ($part ne 'conf-etc') && ($part ne 'conf-lang')); + if (open(PART, "<$filename")) { + while() { + unless ( /^#/ ) { + chomp($contents[$#contents++] = $_); + $content .= $_; + } + } + close PART; + if(wantarray) { + return @contents; + } else { + return $content; + } + } ($self->_seterror($?) && return undef); +} + +# == set files contents == +sub setpart { + my($self, $part, @content) = @_; + my($line); + if(open(PART, ">$self->{'LIST_NAME'}/$part")) { + foreach $line (@content) { + $line =~ s/[\r]//g; $line =~ s/\n$//; + print PART "$line\n"; + } + close PART; + return 1; + } ($self->_seterror($?) && return undef); +} + +# == get the configuration directory for this list (idx >= 5.0) == +# return '/etc/ezmlm' for idx < 5.0 +sub get_config_dir { + my $self = shift; + my $conf_dir; + if ((get_version() >= 5) && (ref $self) && (-e "$self->{'LIST_NAME'}/conf-etc")) { + chomp($conf_dir = $self->getpart('conf-etc')); + } else { + $conf_dir = '/etc/ezmlm'; + } + return $conf_dir; +} + +# == set the configuration directory for this list (idx >= 5.0) == +# return without error for idx < 5.0 +sub set_config_dir { + my ($self, $conf_dir) = @_; + return (0==0) if (get_version() < 5); + $self->setpart('conf-etc', "$conf_dir"); +} + + +# == get list of available languages (for idx >= 5.0) == +# return empty list for idx < 5.0 +sub get_available_languages { + my $self = shift; + my @langs = (); + return @langs if (get_version() < 5); + + $self->_seterror(undef) if (ref $self); + + # check for language directories + my $conf_dir; + if (ref $self) { + ($self->_seterror(-1, 'could not retrieve configuration directory') && return 0) + unless ($conf_dir = $self->get_config_dir()); + } else { + $conf_dir = get_config_dir(); + } + if (opendir DIR, "$conf_dir") { + my @dirs; + @dirs = grep !/^\./, readdir DIR; + closedir DIR; + my $item; + foreach $item (@dirs) { + push (@langs, $item) if (-e "$conf_dir/$item/text"); + } + return @langs; + } else { + $self->_seterror(-1, 'could not access configuration directory') if (ref $self); + return undef; + } +} + + +# == get the selected language of the list (idx >= 5.0) == +# return empty string for idx < 5.0 +sub get_lang { + my ($self) = shift; + my $lang; + return '' if (get_version() < 5); + if (-e "$self->{'LIST_NAME'}/conf-lang") { + chomp($lang = $self->getpart('conf-lang')); + } else { + $lang = 'default'; + } + return $lang; +} + + +# == set the selected language of the list (idx >= 5.0) == +# return without error for idx < 5.0 +sub set_lang { + my ($self, $lang) = @_; + return (0==0) if (get_version() < 5); + if (($lang eq 'default') || ($lang eq '')) { + return 1 if (unlink "$self->{'LIST_NAME'}/conf-lang"); + } else { + return 1 if ($self->setpart('conf-lang', "$lang")); + } + return 0; +} + + +# == get the selected charset of the list == +# return default value (us-ascii) if no charset is specified +sub get_charset { + my ($self) = shift; + my $charset; + $charset = $self->getpart('charset'); + $charset = '' unless defined($charset); + # default if no 'charset' file exists + $charset = 'us-ascii' if ($charset eq ''); + return $charset; +} + + +# == set the selected charset of the list (idx >= 5.0) == +# remove list' specific charset file, if the default charset of the current language +# was chosen +sub set_charset { + my ($self, $charset) = @_; + # first: remove current charset + unlink "$self->{'LIST_NAME'}/charset"; + # second: get default value of the current language + my $default_charset = $self->getpart('charset'); + # last: create new charset file only if the selected charset is not the default anyway + if (($charset eq $default_charset) || ($charset !~ /\S/)) { + # do not write the specific charset, as the default charset of the language is + # sufficient + return 1; + } else { + return 1 if ($self->setpart('charset', "$charset")); + } + return 0; +} + + +# == get list of available text files == +sub get_available_text_files { + my ($self) = shift; + my @files; + my $item; + my %seen = (); + + # customized text files of this list (idx >= 5.0) + # OR text files of this list (idx < 5.0) + if (opendir DIR, "$self->{'LIST_NAME'}/text") { + my @local_files = grep !/^\./, readdir DIR; + closedir DIR; + foreach $item (@local_files) { + unless ($seen{$item}) { + push (@files, $item); + $seen{$item} = 1; + } + } + } + + # default text files (only idx >= 5.0) + if (get_version() >= 5) { + my $dirname = $self->get_config_dir . '/' . $self->get_lang() . '/text'; + $dirname = $self->get_config_dir . '/default/text' unless (-e $dirname); + if (opendir GLOBDIR, $dirname) { + my @global_files = grep !/^\./, readdir GLOBDIR; + closedir GLOBDIR; + foreach $item (@global_files) { + unless ($seen{$item}) { + push (@files, $item); + $seen{$item} = 1; + } + } + } + } + + if ($#files > 0) { + return @files; + } else { + $self->_seterror(-1, 'no textfiles found'); + return undef; + } +} + +# == get text file content == +sub get_text_content { + my ($self, $textfile) = @_; + + if (-e "$self->{'LIST_NAME'}/text/$textfile") { + return $self->getpart("text/$textfile"); + } elsif (get_version() >= 5) { + my $filename = $self->get_config_dir() . '/' . $self->get_lang() . "/text/$textfile"; + $filename = "/etc/ezmlm/default/$textfile" unless (-e "$filename"); + my @contents; + my $content; + if (open(PART, "<$filename")) { + while() { + chomp($contents[$#contents++] = $_); + $content .= $_; + } + close PART; + if(wantarray) { + return @contents; + } else { + return $content; + } + } else { + $self->_seterror($?, "could not open $filename"); + return undef; + } + } else { + $self->_seterror(-1, "could not get the text file ($textfile)"); + return undef; + } +} + + +# == set text file content == +sub set_text_content { + my ($self, $textfile, @content) = @_; + mkdir "$self->{'LIST_NAME'}/text" unless (-e "$self->{'LIST_NAME'}/text"); + return 1 if ($self->setpart("text/$textfile", @content)); + return 0; +} + + +# == check if specified text file is customized or default (for idx >= 5.0) == +# return whether the text file exists in the list's directory (false) or not (true) +# empty filename returns false +sub is_text_default { + my ($self, $textfile) = @_; + return (0==1) if ($textfile eq ''); + if (-e "$self->{'LIST_NAME'}/text/$textfile") { + return (1==0); + } else { + return (0==0); + } +} + + +# == remove non-default text file (for idx >= 5.0) == +# return without error for idx < 5 +# otherwise: remove customized text file from the list's directory +sub reset_text { + my ($self, $textfile) = @_; + return if (get_version() < 5); + return if ($textfile eq ''); + return if ($textfile =~ /[^\w_\.-]/); + return if ($self->is_text_default($textfile)); + ($self->_seterror(-1, "could not remove customized text file ($textfile)") && return 0) + unless unlink("$self->{'LIST_NAME'}/text/$textfile"); + return 1; +} + + +# == return an error message if appropriate == +sub errmsg { + my($self) = @_; + return $self->{'ERRMSG'}; +} + +sub errno { + my($self) = @_; + return $self->{'ERRNO'}; +} + +# == Test the compatiblity of the module == +sub check_version { + my $self = shift; + my $version = `$EZMLM_BASE/ezmlm-make -V 2>&1`; + $self->_seterror(undef) if (ref $self); + + # ezmlm-idx is necessary + if (get_version() >= 4) { + return 0; + } else { + return $version; + } +} + +# == get the major ezmlm version == +# return values: +# 0 => unknown version +# 3 => ezmlm v0.53 +# 4 => ezmlm-idx v0.4* +# 5 => ezmlm-idx v5.0 +# 5.1 => ezmlm-idx v5.1 +# 6 => ezmlm-idx v6.* +# 7 => ezmlm-idx v7.* +sub get_version { + my ($ezmlm, $idx); + my $version = `$EZMLM_BASE/ezmlm-make -V 2>&1`; + + $version = $1 if ($version =~ m/^[^:]*:\s+(.*)$/); + $ezmlm = $1 if ($version =~ m/ezmlm-([\d\.]+)$/); + $idx = $1 if ($version =~ m/ezmlm-idx-([\d\.]+)$/); + + if (defined($ezmlm)) { + return 3; + } elsif (defined($idx)) { + if (($idx =~ m/^(\d)/) && ($1 >= 7)) { + # version 6.0 or higher + return 7; + } elsif (($idx =~ m/^(\d)/) && ($1 == 6)){ + return 6; + } elsif (($idx =~ m/^(\d)\.(\d)/) && ($1 >= 5) && ($2 == 1)) { + # version 5.1 + return 5.1; + } elsif (($idx =~ m/^(\d)/) && ($1 >= 5)) { + # version 5.0 + return 5; + } elsif (($idx =~ m/^0\.(\d)/) && ($1 >= 0)) { + # version 0.4xx + return 4; + } else { + return 0; + } + } else { + return 0; + } +} + +# == Create SQL Database tables if defined for a list == +sub createsql { + my($self) = @_; + + ($self->_seterror(-1, 'MySQL must be compiled into Ezmlm for createsql() to work') && return 0) unless(defined($MYSQL_BASE) && $MYSQL_BASE); + ($self->_seterror(-1, 'must setlist() before isdigest()') && return 0) unless(defined($self->{'LIST_NAME'})); + my($config) = $self->getconfig(); + + if($config =~ m/-6\s+'(.+?)'\s*/){ + my($sqlsettings) = $1; + my($host, $port, $user, $password, $database, $table) = split(':', $sqlsettings, 6); + + ($self->_seterror(-1, 'error in list configuration while trying createsql()') && return 0) + unless (defined($host) && defined($port) && defined($user) + && defined($password) && defined($database) && defined($table)); + + system("$EZMLM_BASE/ezmlm-mktab -d $table | $MYSQL_BASE/mysql -h$host -P$port -u$user -p$password -f $database") == 0 || + ($self->_seterror($?) && return undef); + + } else { + $self->_seterror(-1, 'config for thislist() must include SQL options'); + return 0; + } + + ($self->_seterror(undef) && return 1); + +} + + +# == Internal function to set the error to return == +sub _seterror { + my($self, $no, $mesg) = @_; + + if(defined($no) && $no) { + if($no < 0) { + $self->{'ERRNO'} = -1; + $self->{'ERRMSG'} = $mesg || 'An undefined error occoured'; + } else { + $self->{'ERRNO'} = $no / 256; + $self->{'ERRMSG'} = $! || $mesg || 'An undefined error occoured in a system() call'; + } + } else { + $self->{'ERRNO'} = 0; + $self->{'ERRMSG'} = undef; + } + return 1; +} + +# == Internal function to test for valid email addresses == +sub _checkaddress { + my($self, $address) = @_; + return 1 unless defined($address); + return 0 unless ($address =~ m/^(\S+\@\S+\.\S+)$/); + $_[1] = $1; + return 1; +} + +# == Internal function to work out a list configuration (idx >= v5.0) == +sub _getconfig_idx5 { + my($self) = @_; + my ($options, %optionfiles); + my ($file, $opt_num, $temp); + + # read flag file (available since ezmlm-idx 5.0.0) + chomp($options = $self->getpart('flags')); + # remove prefixed '-' + $options =~ s/^-//; + + # since ezmlm-idx v5, we have to read the config + # values from different files + # first: preset a array with "filename" and "option_number" + %optionfiles = ( + 'sublist', 0, + 'fromheader', 3, + 'tstdigopts', 4, + 'owner', 5, + 'sql', 6, + 'modpost', 7, + 'modsub', 8, + 'remote', 9); + while (($file, $opt_num) = each(%optionfiles)) { + if (-e "$self->{'LIST_NAME'}/$file") { + chomp($temp = $self->getpart($file)); + $temp =~ m/^(.*)$/m; # take only the first line + $temp = $1; + # the 'owner' setting can be ignored if it is a path (starts with '/') + unless (($opt_num == 5) && ($temp =~ m#^/#)) { + $options .= " -$opt_num '$temp'" if ($temp =~ /\S/); + } + } + } + + return $options; +} + +# == Internal function to work out a list configuration manually (idx < v5.0.0 ) == +sub _getconfigmanual { + # use this function for strange lists without + # 'config' (idx < v5.0) and 'flags' (idx >= v5.0) + my($self) = @_; + my ($savedollarslash, $options, $manager, $editor, $i); + + # Read the whole of DIR/editor and DIR/manager in + $savedollarslash = $/; + undef $/; + # $/ = \0777; + + open (EDITOR, "<$self->{'LIST_NAME'}/editor") || ($self->_seterror($?) && return undef); + open (MANAGER, "<$self->{'LIST_NAME'}/manager") || ($self->_seterror($?) && return undef); + $editor = ; $manager = ; + close(EDITOR), close(MANAGER); + + $/ = $savedollarslash; + + $options = ''; + $options .= 'a' if (-e "$self->{'LIST_NAME'}/archived"); + $options .= 'd' if (-e "$self->{'LIST_NAME'}/digest"); + $options .= 'f' if (-e "$self->{'LIST_NAME'}/prefix"); + $options .= 'g' if ($manager =~ /ezmlm-get -\w*s/ ); + $options .= 'i' if (-e "$self->{'LIST_NAME'}/indexed"); + $options .= 'k' if (-e "$self->{'LIST_NAME'}/blacklist" || -e "$self->{'LIST_NAME'}/deny"); + $options .= 'l' if ($manager =~ /ezmlm-manage -\w*l/ ); + $options .= 'm' if (-e "$self->{'LIST_NAME'}/modpost"); + $options .= 'n' if ($manager =~ /ezmlm-manage -\w*e/ ); + $options .= 'p' if (-e "$self->{'LIST_NAME'}/public"); + $options .= 'q' if ($manager =~ /ezmlm-request/ ); + $options .= 'r' if (-e "$self->{'LIST_NAME'}/remote"); + $options .= 's' if (-e "$self->{'LIST_NAME'}/modsub"); + $options .= 't' if (-e "$self->{'LIST_NAME'}/text/trailer"); + $options .= 'u' if (($options !~ /m/ && $editor =~ /ezmlm-issubn \'/ ) + || $editor =~ /ezmlm-gate/ ); + $options .= 'x' if (-e "$self->{'LIST_NAME'}/extra" || -e "$self->{'LIST_NAME'}/allow"); + + # Add the unselected options too + # but we will skip invalid options (any of 'cevz') + foreach $i ('a' .. 'z') { + $options .= uc($i) unless (('cevz' =~ /$i/) || ($options =~ /$i/i)) + } + + # there is no way to get the other string settings, that are only + # defined in 'config' - sorry ... + + return $options; +} + +# == Internal Function to try to determine the vhost user == +sub _getvhostuser { + my($self, $hostname) = @_; + my($username); + + open(VD, "<$QMAIL_BASE/control/virtualdomains") || ($self->_seterror($?) && return undef); + while() { + last if(($username) = /^\s*$hostname:(\w+)$/); + } + close VD; + + return $username; +} + +# == Internal function to work out default host name == +sub _getdefaultdomain { + my($self) = @_; + my($hostname); + + open (GETHOST, "<$QMAIL_BASE/control/defaultdomain") + || open (GETHOST, "<$QMAIL_BASE/control/me") + || ($self->_seterror($?) && return undef); + chomp($hostname = ); + close GETHOST; + + return $hostname; +} + +1; +__END__ + +=head1 NAME + +Ezmlm - Object Methods for Ezmlm Mailing Lists + +=head1 SYNOPSIS + + use Mail::Ezmlm; + $list = new Mail::Ezmlm; + +The rest is a bit complicated for a Synopsis, see the description. + +=head1 ABSTRACT + +Ezmlm is a Perl module that is designed to provide an object interface to +the ezmlm mailing list manager software. See the ezmlm web page +(http://www.ezmlm.org/) for a complete description of the software. + +This version of the module is designed to work with ezmlm version 0.53. +It is fully compatible with ezmlm's IDX extensions (version 0.4xx and 5.0 ). Both +of these can be obtained via anon ftp from ftp://ftp.ezmlm.org/pub/patches/ + +=head1 DESCRIPTION + +=head2 Setting up a new Ezmlm object: + + use Mail::Ezmlm; + $list = new Mail::Ezmlm; + $list = new Mail::Ezmlm('/home/user/lists/moolist'); + +=head2 Changing which list the Ezmlm object points at: + + + $list->setlist('/home/user/lists/moolist'); + +=head2 Getting a list of current subscribers: + +=item Two methods of listing subscribers is provided. The first prints a list +of subscribers, one per line, to the supplied FILEHANDLE. If no filehandle is +given, this defaults to STDOUT. An optional second argument specifies the +part of the list to display (mod, digest, allow, deny). If the part is +specified, then the FILEHANDLE must be specified. + + $list->list; + $list->list(\*STDERR); + $list->list(\*STDERR, 'deny'); + +=item The second method returns an array containing the subscribers. The +optional argument specifies which part of the list to display (mod, digest, +allow, deny). + + @subscribers = $list->subscribers; + @subscribers = $list->subscribers('allow'); + +=head2 Testing for subscription: + + $list->issub('nobody@on.web.za'); + $list->issub(@addresses); + $list->issub(@addresses, 'mod'); + +issub() returns 1 if all the addresses supplied are found as subscribers +of the current mailing list, otherwise it returns undefined. The optional +argument specifies which part of the list to check (mod, digest, allow, +deny). + +=head2 Subscribing to a list: + + $list->sub('nobody@on.web.za'); + $list->sub(@addresses); + $list->sub(@addresses, 'digest'); + +sub() takes a LIST of addresses and subscribes them to the current mailing list. +The optional argument specifies which part of the list to subscribe to (mod, +digest, allow, deny). + + +=head2 Unsubscribing from a list: + + $list->unsub('nobody@on.web.za'); + $list->unsub(@addresses); + $list->unsub(@addresses, 'mod'); + +unsub() takes a LIST of addresses and unsubscribes them (if they exist) from the +current mailing list. The optional argument specifies which part of the list +to unsubscribe from (mod, digest, allow, deny). + + +=head2 Creating a new list: + + $list->make(-dir=>'/home/user/list/moo', + -qmail=>'/home/user/.qmail-moo', + -name=>'user-moo', + -host=>'on.web.za', + -user=>'onwebza', + -switches=>'mPz'); + +make() creates the list as defined and sets it to the current list. There are +three variables which must be defined in order for this to occur; -dir, -qmail and -name. + +=over 6 + +=item -dir is the full path of the directory in which the mailing list is to +be created. + +=item -qmail is the full path and name of the .qmail file to create. + +=item -name is the local part of the mailing list address (eg if your list +was user-moo@on.web.za, -name is 'user-moo'). + +=item -host is the name of the host that this list is being created on. If +this item is omitted, make() will try to determine your hostname. If -host is +not the same as your hostname, then make() will attempt to fix DIR/inlocal for +a virtual host. + +=item -user is the name of the user who owns this list. This item only needs to +be defined for virtual domains. If it exists, it is prepended to -name in DIR/inlocal. +If it is not defined, the make() will attempt to work out what it should be from +the qmail control files. + +=item -switches is a list of command line switches to pass to ezmlm-make(1). +Note that the leading dash ('-') should be ommitted from the string. + +=back + +make() returns the value of thislist() for success, undefined if there was a +problem with the ezmlm-make system call and 0 if there was some other problem. + +See the ezmlm-make(1) man page for more details + +=head2 Determining which list we are currently altering: + + $whichlist = $list->thislist; + print $list->thislist; + +=head2 Getting the current configuration of the current list: + + $list->getconfig; + +getconfig() returns a string that contains the command line switches that +would be necessary to re-create the current list. It does this by reading the +DIR/config file (idx < v5.0) or DIR/flags (idx >= v5.0) if one of them exists. +If it can't find these files it attempts to work things out for itself (with +varying degrees of success). If both these methods fail, then getconfig() +returns undefined. + + $list->ismodpost; + $list->ismodsub; + $list->isremote; + $list->isdeny; + $list->isallow; + +The above five functions test various features of the list, and return a 1 +if the list has that feature, or a 0 if it doesn't. These functions are +considered DEPRECATED as their result is not reliable. Use "getconfig" instead. + +=head2 Updating the configuration of the current list: + + $list->update('msPd'); + +update() can be used to rebuild the current mailing list with new command line +options. These options can be supplied as a string argument to the procedure. +Note that you do not need to supply the '-' or the 'e' command line switch. + + @part = $list->getpart('headeradd'); + $part = $list->getpart('headeradd'); + $list->setpart('headerremove', @part); + +getpart() and setpart() can be used to retrieve and set the contents of +various text files such as headeradd, headerremove, mimeremove, etc. + +=head2 Manage language dependent text files + + $list->get_available_text_files; + $list->get_text_content('sub-ok'); + $list->set_text_content('sub-ok', @content); + +These functions allow you to manipulate the text files, that are used for +automatic replies by ezmlm. + + $list->is_text_default('sub-ok'); + $list->reset_text('sub-ok'); + +These two functions are available if you are using ezmlm-idx v5.0 or higher. +is_text_default() checks, if there is a customized text file defined for this list. +reset_text() removes the customized text file from this list. Ezmlm-idx will use +system-wide default text file, if there is no customized text file for this list. + +=head2 Change the list's settings (for ezmlm-idx >= 5.0) + + Mail::Ezmlm->get_config_dir; + $list->get_config_dir; + $list->set_config_dir('/etc/ezmlm-local'); + +These functions access the file 'conf-etc' in the mailing list's directory. The +static function (first example) always returns the default configuration directory +of ezmlm-idx (/etc/ezmlm). + + $list->get_available_languages; + $list->get_lang; + $list->set_lang('de'); + $list->get_charset; + $list->set_charset('iso-8859-1:Q'); + +These functions allow you to change the language of the text files, that are used +for automatic replies of ezmlm-idx (since v5.0 the configured language is stored +in 'conf-lang' within the mailing list's directory). Customized files (in the 'text' +directory of a mailing list directory) override the default language files. +Empty strings for set_lang() and set_charset() reset the setting to its default value. + +=head2 Get the installed version of ezmlm + + Mail::Ezmlm->get_version; + +The result is one of the following: + 0 - unknown + 3 - ezmlm 0.53 + 4 - ezmlm-idx 0.4xx + 5 - ezmlm-idx 5.x + 5.1 - ezmlm-idx 5.1 + 6 - ezmlm-idx 6.x + 7 - ezmlm-idx 7.x + +=head2 Creating MySQL tables: + + $list->createsql(); + +Currently only works for MySQL. + +createsql() will attempt to create the table specified in the SQL connect +options of the current mailing list. It will return an error if the current +mailing list was not configured to use SQL, or is Ezmlm was not compiled +with MySQL support. See the MySQL info pages for more information. + +=head2 Checking the Mail::Ezmlm and ezmlm version numbers + +The version number of the Mail::Ezmlm module is stored in the variable +$Mail::Ezmlm::VERSION. The compatibility of this version of Mail::Ezmlm +with your system installed version of ezmlm can be checked with + + $list->check_version(); + +This returns 0 for compatible, or the version string of ezmlm-make(2) if +the module is incompatible with your set up. + +=head1 RETURN VALUES + +All of the routines described above have return values. 0 or undefined are +used to indicate that an error of some form has occoured, while anything +>0 (including strings, etc) are used to indicate success. + +If an error is encountered, the functions + + $list->errno(); + $list->errmsg(); + +can be used to determine what the error was. + +errno() returns; 0 or undef if there was no error. + -1 for an error relating to this module. + >0 exit value of the last system() call. + +errmsg() returns a string containing a description of the error ($! if it +was from a system() call). If there is no error, it returns undef. + +For those who are interested, in those sub routines that have to make system +calls to perform their function, an undefined value indicates that the +system call failed, while 0 indicates some other error. Things that you would +expect to return a string (such as thislist()) return undefined to indicate +that they haven't a clue ... as opposed to the empty string which would mean +that they know about nothing :) + +=head1 AUTHOR + + Guy Antony Halse + Lars Kruse + +=head1 BUGS + + There are no known bugs. + + Please report bugs to the author or use the bug tracking system at + https://systemausfall.org/trac/ezmlm-web. + +=head1 SEE ALSO + + ezmlm(5), ezmlm-make(2), ezmlm-sub(1), + ezmlm-unsub(1), ezmlm-list(1), ezmlm-issub(1) + + http://rucus.ru.ac.za/~guy/ezmlm/ + https://systemausfall.org/toolforge/ezmlm-web + http://www.ezmlm.org/ + http://www.qmail.org/ + +=cut diff --git a/Ezmlm/tags/Ezmlm-0.08/Ezmlm/GpgEzmlm.pm b/Ezmlm/tags/Ezmlm-0.08/Ezmlm/GpgEzmlm.pm new file mode 100644 index 0000000..90797c5 --- /dev/null +++ b/Ezmlm/tags/Ezmlm-0.08/Ezmlm/GpgEzmlm.pm @@ -0,0 +1,887 @@ +# =========================================================================== +# GpgEzmlm.pm +# $Id$ +# +# Object methods for gpg-ezmlm mailing lists +# +# Copyright (C) 2006, Lars Kruse, All Rights Reserved. +# Please send bug reports and comments to devel@sumpfralle.de +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +# +# ========================================================================== + +package Mail::Ezmlm::GpgEzmlm; + +use strict; +use warnings; +use diagnostics; +use vars qw($GPG_EZMLM_BASE $GPG_BIN $VERSION @ISA @EXPORT @EXPORT_OK); +use File::Copy; +use Carp; + +use Mail::Ezmlm; + +# this package inherits object methods from Mail::Ezmlm +@ISA = qw(Mail::Ezmlm); + +$VERSION = '0.1'; + +require 5.005; + +=head1 NAME + +Mail::Ezmlm::GpgEzmlm - Object Methods for encrypted Ezmlm Mailing Lists + +=head1 SYNOPSIS + + use Mail::Ezmlm::GpgEzmlm; + $list = new Mail::Ezmlm::GpgEzmlm(DIRNAME); + +The rest is a bit complicated for a Synopsis, see the description. + +=head1 DESCRIPTION + +Mail::Ezmlm::GpgEzmlm is a Perl module that is designed to provide an object +interface to encrypted mailing lists based upon gpg-ezmlm. +See the gpg-ezmlm web page (http://www.synacklabs.net/projects/crypt-ml/) for +details about this software. + +The Mail::Ezmlm::GpgEzmlm class is inherited from the Mail::Ezmlm class. + +=cut + +# == Begin site dependant variables == +$GPG_EZMLM_BASE = '/usr/bin'; # Autoinserted by Makefile.PL +$GPG_BIN = '/usr/bin/gpg'; # Autoinserted by Makefile.PL + +# == clean up the path for taint checking == +local $ENV{PATH}; +# the following lines were taken from "man perlrun" +$ENV{PATH} = $GPG_EZMLM_BASE; +$ENV{SHELL} = '/bin/sh' if exists $ENV{SHELL}; +delete @ENV{qw(IFS CDPATH ENV BASH_ENV)}; + + +# check, if gpg-ezmlm is installed +unless (-x "$GPG_EZMLM_BASE/gpg-ezmlm-manage.pl") { + die("Warning: gpg-ezmlm does not seem to be installed - " + . "executable '$GPG_EZMLM_BASE/gpg-ezmlm-manage.pl' not found!"); +} + + +# == Initialiser - Returns a reference to the object == + +=head2 Setting up a new Mail::Ezmlm::GpgEzmlm object: + + use Mail::Ezmlm::GpgEzmlm; + $list = new Mail::Ezmlm::GpgEzmlm('/home/user/lists/moolist'); + +new() returns undefined if an error occoured. + +Use this function to access an existing encrypted mailing list. + +=cut + +sub new { + my ($class, $list_dir) = @_; + # call the previous initialization function + my $self = $class->SUPER::new($list_dir); + bless $self, ref $class || $class || 'Mail::Ezmlm::GpgEzmlm'; + # define the available (supported) options for gpg-ezmlm == + @{$self->{SUPPORTED_OPTIONS}} = ( + "GnuPG", + "KeyDir", + "RequireSub", + "RequireSigs", + "NoKeyNoCrypt", + "SignMessages", + "EncryptToAll", + "VerifiedKeyReq", + "AllowKeySubmission"); + # check if the mailing is encrypted + if (_is_encrypted($list_dir)) { + return $self; + } else { + return undef; + } +} + +# == convert an existing list to gpg-ezmlm == + +=head2 Converting a plaintext mailing list to an encrypted list: + +You need to have a normal list before you can convert it into an encrypted list. +You can create plaintext mailing list with Mail::Ezmlm. + + $encrypted_list->Mail::Ezmlm::GpgEzmlm->convert_to_encrypted('/lists/foo'); + +Use this function to convert a plaintext list into an encrypted mailing list. +The function returns a Mail::Ezmlm::GpgEzmlm object if it was successful. +Otherwise it returns undef. + +=cut + +sub convert_to_encrypted { + my $class = shift; + my $list_dir = shift; + my ($backup_dir); + + # untaint "list_dir" + $list_dir =~ m/^([\w\d\_\-\.\@ \/]+)$/; + if (defined($1)) { + $list_dir = $1; + } else { + warn "[GpgEzmlm] list directory contains invalid characters!"; + return undef; + } + + # the backup directory will contain the old config file and the dotqmails + $backup_dir = _get_config_backup_dir($list_dir); + if ((! -e $backup_dir) && (!mkdir($backup_dir))) { + warn "[GpgEzmlm] failed to create gpg-ezmlm conversion backup dir (" + . "$backup_dir): $!"; + return undef; + } + + # check the input + unless (defined($list_dir)) { + warn '[GpgEzmlm] must define directory in convert_to_encrypted()'; + return undef; + } + + # does the list directory exist? + unless (-d $list_dir) { + warn '[GpgEzmlm] directory does not exist: ' . $list_dir; + return undef; + } + + # the list should currently _not_ be encrypted + if (_is_encrypted($list_dir)) { + warn '[GpgEzmlm] list is already encrypted: ' . $list_dir; + return undef; + } + + + # here starts the real conversion - the code is based on + # "gpg-ezmlm-convert.pl" - see http://www.synacklabs.net/projects/crypt-ml/ + + # update the dotqmail files + return undef unless (_cleanup_dotqmail_files($list_dir, $backup_dir)); + + # create the new config file, if it did not exist before + unless (-e "$backup_dir/config.gpg-ezmlm") { + if (open(CONFIG_NEW, ">$backup_dir/config.gpg-ezmlm")) { + # just create the empty file (default) + close CONFIG_NEW; + } else { + warn "[GpgEzmlm] failed to create new config file (" + . "$backup_dir/config.gpg-ezmlm): $!"; + return undef; + } + } + + return undef unless (&_enable_encryption_config_file($list_dir)); + + # create the (empty) gnupg keyring directory - this enables the keyring + # management interface. Don't create it, if it already exists. + if ((!-e "$list_dir/.gnupg") && (!mkdir("$list_dir/.gnupg", 0700))) { + warn "[GpgEzmlm] failed to create the gnupg keyring directory: $!"; + return undef; + } + + my $result = $class->new($list_dir); + return $result; +} + +# == convert an encrypted list back to plaintext == + +=head2 Converting an encryted mailing list to a plaintext list: + + $list->convert_to_plaintext(); + +This function returns undef in case of errors. Otherwise the Mail::Ezmlm +object of the plaintext mailing list is returned. + +=cut + +sub convert_to_plaintext { + my $self = shift; + my ($dot_loc, $list_dir, $dot_prefix, $backup_dir); + + $list_dir = $self->thislist(); + # untaint the input + $list_dir =~ m/^([\w\d\_\-\.\/\@]+)$/; + unless (defined($1)) { + # sanitize directory name (it must be safe to put the warn message) + $list_dir =~ s/\W/_/g; + warn "[GpgEzmlm] the list directory contains invalid characters: '" + . $list_dir . "' (special characters are escaped)"; + return undef; + } + $list_dir = $1; + + # check if a directory was given + unless (defined($list_dir)) { + $self->_seterror(-1, 'must define directory in convert_to_plaintext()'); + return undef; + } + # the list directory must exist + unless (-d $list_dir) { + $self->_seterror(-1, 'directory does not exist: ' . $list_dir); + return undef; + } + # check if the current object is still encrypted + unless (_is_encrypted($list_dir)) { + $self->_seterror(-1, 'list is not encrypted: ' . $list_dir); + return undef; + } + + # retrieve location of dotqmail-files + $dot_loc = _get_dotqmail_location($list_dir); + + # untaint "dot_loc" + $dot_loc =~ m/^([\w\d\_\-\.\@ \/]+)$/; + if (defined($1)) { + $dot_loc = $1; + } else { + $dot_loc =~ s/\W/_/g; + warn "[GpgEzmlm] directory name of dotqmail files contains invalid " + . "characters: $dot_loc (special characters are escaped)"; + return undef; + } + + # the backup directory should contain the old config file (if it existed) + # and the original dotqmail files + $backup_dir = _get_config_backup_dir($self->thislist()); + unless (-r $backup_dir) { + warn "[GpgEzmlm] failed to revert conversion - the backup directory " + . "is missing: $backup_dir"; + return undef; + } + + # the "dot_prefix" is the basename of the main dotqmail file + # (e.g. '.qmail-list-foo') + $dot_loc =~ m/\/([^\/]+)$/; + if (defined($1)) { + $dot_prefix = $1; + } else { + warn '[GpgEzmlm] invalid location of dotqmail file: ' . $dot_loc; + return undef; + } + + # the "dotqmail" location must be valid + unless (defined($dot_loc) && ($dot_loc ne '') && (-e $dot_loc)) { + $self->_seterror(-1, 'dotqmail files not found: ' . $dot_loc); + return undef; + } + + # start reverting the gpg-ezmlm conversion: + # - restore old dotqmail files + # - restore old config file (if it existed before) + + # restore original config file (if it exists) + &_enable_plaintext_config_file($list_dir); + + # replace the dotqmail files with the ones from the backup + unless ((File::Copy::copy("$backup_dir/$dot_prefix", "$dot_loc")) + && (File::Copy::copy("$backup_dir/$dot_prefix-default", + "$dot_loc-default",))) { + warn "[GpgEzmlm] failed to restore dotqmail files: $!"; + return undef; + } + + $self = Mail::Ezmlm->new($list_dir); + return $self; +} + +# == Update the "normal" settings of the current list == + +=head2 Updating the common configuration settings of the current list: + + $list->update("moUx"); + +=cut + +# update the "normal" (=not related to encryption) settings of the list +sub update { + my $self = shift; + my $options = shift; + + my ($result); + + + # restore the ususal ezmlm-idx config file (for v0.4xx) + &_enable_plaintext_config_file($self->thislist()); + # let ezmlm-make do the setup + $result = $self->SUPER::update($options); + # restore the gpg-ezmlm config file + &_enable_encryption_config_file($self->thislist()); + # "repair" the dotqmail files (use "gpg-ezmlm-send" instead of "ezmlm-send") + &_cleanup_dotqmail_files($self->thislist()); + + # return the result of the ezmlm-make run + return $result; +} + +# == Update the encryption settings of the current list == + +=head2 Updating the configuration of the current list: + + $list->update_special({ 'allowKeySubmission' => 1 }); + +=cut + +# update the encryption specific settings +sub update_special { + my ($self, %switches) = @_; + my (%ok_switches, $one_key, @delete_switches); + + # check for important files: 'config' + unless (_is_encrypted($self->thislist())) { + $self->_seterror(-1, "Update failed: '" . $self->thislist() + . "' does not appear to be a valid list"); + return undef; + } + + @delete_switches = (); + # check if all supplied settings are supported + # btw we change the case (upper/lower) of the setting to the default one + foreach $one_key (keys %switches) { + my $ok_key; + foreach $ok_key (@{$self->{SUPPORTED_OPTIONS}}) { + # check the key case-insensitively + if ($ok_key =~ /^$one_key$/i) { + $ok_switches{$ok_key} = $switches{$one_key}; + push @delete_switches, $one_key; + } + } + } + # remove all keys, that were accepted above + # we could not do it before, since this could cause issues with the current + # "foreach" looping through the hash + foreach $one_key (@delete_switches) { + delete $switches{$one_key}; + } + + # %switches should be empty now + if (%switches) { + foreach $one_key (keys %switches) { + warn "[GpgEzmlm] unsupported setting: $one_key"; + } + } + + my $errorstring; + my $config_file_old = $self->thislist() . "/config"; + my $config_file_new = $self->thislist() . "/config.new"; + my $gnupg_setting_found = (0==1); + if (open(CONFIG_OLD, "<$config_file_old")) { + if (open(CONFIG_NEW, ">$config_file_new")) { + my ($in_line, $one_opt, $one_val, $new_setting); + while () { + $in_line = $_; + $gnupg_setting_found = (0==0) if ($in_line =~ m/^\s*GnuPG\s+/i); + if (%ok_switches) { + my $found = 0; + while (($one_opt, $one_val) = each(%ok_switches)) { + # is this the right line (maybe commented out)? + if ($in_line =~ m/^#?\s*$one_opt\s+/i) { + print CONFIG_NEW _get_config_line($one_opt, $one_val); + delete $ok_switches{$one_opt}; + $found = 1; + } + } + print CONFIG_NEW $in_line if ($found == 0); + } else { + # just print the remaining config file if no other settings are left + print CONFIG_NEW $in_line; + } + } + # write the remaining settings to the end of the file + while (($one_opt, $one_val) = each(%ok_switches)) { + print CONFIG_NEW _get_config_line($one_opt, $one_val); + } + # always set the default value for the "gpg" setting explicitely, + # if it was not overriden - otherwise gpg-ezmlm breaks on most + # systems (its default location is /usr/local/bin/gpg) + unless ($gnupg_setting_found) { + print CONFIG_NEW _get_config_line("GnuPG", $GPG_BIN); + } + } else { + $errorstring = "failed to write to temporary config file: $config_file_new"; + $self->_seterror(-1, $errorstring); + warn "[GpgEzmlm] $errorstring"; + close CONFIG_OLD; + return (1==0); + } + close CONFIG_NEW; + } else { + $errorstring = "failed to read the config file: $config_file_old"; + $self->_seterror(-1, $errorstring); + warn "[GpgEzmlm] $errorstring"; + return (1==0); + } + close CONFIG_OLD; + unless (rename($config_file_new, $config_file_old)) { + $errorstring = "failed to move new config file ($config_file_new) " + . "to original config file ($config_file_old)"; + $self->_seterror(-1, $errorstring); + warn "[GpgEzmlm] $errorstring"; + return (1==0); + } + $self->_seterror(undef); + return (0==0); +} + + +# return the configuration file string for a key/value combination +sub _get_config_line { + my $key = shift; + my $value = shift; + + my $result = "$key "; + if (($key eq "GnuPG") || ($key eq "keyDir")) { + # these are the only settings with string values + # escape special characters + $value =~ s/[^\w\.\/\-]/_/g; + $result .= $value; + } else { + $result .= ($value)? "yes" : "no"; + } + $result .= "\n"; + return $result; +} + +# == Get a list of options for the current list == + +=head2 Getting the current configuration of the current list: + + $list->getconfig; + +getconfig() returns a hash including all available settings +(undefined settings are returned with their default value). + +=cut + +# call the original 'getconfig' function after restoring the "normal" config +# file (necessary only for ezmlm-idx < 0.4x) +sub getconfig { + my $self = shift; + + my ($result); + + &_enable_plaintext_config_file($self->thislist()); + $result = $self->SUPER::getconfig(); + &_enable_encryption_config_file($self->thislist()); + + return $result; +} + +# retrieve the specific configuration of the list +sub getconfig_special { + my ($self) = @_; + my (%options, $list_dir); + + # continue with retrieving the encryption configuration + + # define defaults + $options{KeyDir} = ''; + $options{SignMessages} = 1; + $options{NoKeyNoCrypt} = 0; + $options{AllowKeySubmission} = 1; + $options{EncryptToAll} = 0; + $options{VerifiedKeyReq} = 0; + $options{RequireSub} = 0; + $options{RequireSigs} = 0; + + + # Read the config file + $list_dir = $self->thislist(); + if (open(CONFIG, "<$list_dir/config")) { + # 'config' contains the authorative information + while() { + if (/^(\w+)\s(.*)$/) { + my $optname = $1; + my $optvalue = $2; + my $one_opt; + foreach $one_opt (@{$self->{SUPPORTED_OPTIONS}}) { + if ($one_opt =~ m/^$optname$/i) { + if ($optvalue =~ /^yes$/i) { + $options{$one_opt} = 1; + } else { + $options{$one_opt} = 0; + } + } + } + } + } + close CONFIG; + } else { + $self->_seterror(-1, 'unable to read configuration file in getconfig()'); + return undef; + } + + $self->_seterror(undef); + return %options; +} + + +# ********** internal functions **************** + +# return the location of the dotqmail files +sub _get_dotqmail_location { + my $list_dir = shift; + my ($plain_list, $dot_loc); + + $plain_list = Mail::Ezmlm->new($list_dir); + if ($plain_list) { + if (-r "$list_dir/dot") { + $dot_loc = $plain_list->getpart("dot"); + chomp($dot_loc); + } elsif (-r "$list_dir/config") { + # the "config" file was used before ezmlm-idx v5 + $dot_loc = $1 if ($plain_list->getpart("config") =~ m/^T:(.*)$/); + } else { + warn '[GpgEzmlm] list configuration file not found: ' . $list_dir; + $dot_loc = undef; + } + } else { + # return undef for invalid list directories + $dot_loc = undef; + } + return $dot_loc; +} + + +# return true if the given directory contains a gpg-ezmlm mailing list +sub _is_encrypted { + my $list_dir = shift; + my ($result, $plain_list); + + # by default we assume, that the list is not encrypted + $result = 0; + + if (-e "$list_dir/lock") { + # it is a valid ezmlm-idx mailing list + $plain_list = Mail::Ezmlm->new($list_dir); + if ($plain_list) { + if (-e "$list_dir/config") { + my $content = $plain_list->getpart("config"); + $content = '' unless defined($content); + # return false if we encounter the usual ezmlm-idx-v0.4-header + if ($content =~ /^F:/m) { + # this is a plaintext ezmlm-idx v0.4 mailing list + # this is a valid case - no warning necessary + } else { + # this is a gpg-ezmlm mailing list + $result = 1; + } + } else { + # gpg-ezmlm needs a "config" file - thus the list seems to be plain + # this is a valid case - no warning necessary + } + } else { + # failed to create a plaintext mailing list object + warn "[GpgEzmlm] failed to create Mail::Ezmlm object for: " + . $list_dir; + } + } else { + warn "[GpgEzmlm] Directory does not appear to contain a valid list: " + . $list_dir; + } + + return $result; +} + + +# what is done: +# - copy current dotqmail files to the backup directory +# - replace "ezmlm-send" and "ezmlm-manage" with the gpg-ezmlm replacements +# (in the real dotqmail files) +# This function should be called: +# 1) as part of the plaintext->encryption conversion of a list +# 2) after calling ezmlm-make for an encrypted list (since the dotqmail files +# are overwritten by ezmlm-make) +sub _cleanup_dotqmail_files { + my $list_dir = shift; + my ($backup_dir, $dot_loc, $dot_prefix); + + # where should we store the current dotqmail files? + $backup_dir = _get_config_backup_dir($list_dir); + + # retrieve location of dotqmail-files + $dot_loc = _get_dotqmail_location($list_dir); + + # untaint "dot_loc" + $dot_loc =~ m/^([\w\d\_\-\.\@ \/]+)$/; + if (defined($1)) { + $dot_loc = $1; + } else { + $dot_loc =~ s/\W/_/g; + warn "[GpgEzmlm] directory name of dotqmail files contains invalid " + . "characters: $dot_loc (escaped special characters)"; + return undef; + } + + # the "dot_prefix" is the basename of the main dotqmail file + # (e.g. '.qmail-list-foo') + $dot_loc =~ m/\/([^\/]+)$/; + if (defined($1)) { + $dot_prefix = $1; + } else { + warn '[GpgEzmlm] invalid location of dotqmail file: ' . $dot_loc; + return undef; + } + + # check if the base dotqmail file exists + unless (defined($dot_loc) && ($dot_loc ne '') && (-e $dot_loc)) { + warn '[GpgEzmlm] dotqmail files not found: ' . $dot_loc; + return undef; + } + + # move the base dotqmail file + if (open(DOT_NEW, ">$backup_dir/$dot_prefix.new")) { + if (open(DOT_ORIG, "<$dot_loc")) { + my $line_found = (0==1); + while () { + my $line = $_; + if ($line =~ /ezmlm-send\s+(\S+)/) { + print DOT_NEW "\|$GPG_EZMLM_BASE/gpg-ezmlm-send.pl $1\n"; + $line_found = (0==0); + } else { + print DOT_NEW $line; + } + } + close DOT_ORIG; + # move the original file to the backup and the new file back + if ($line_found) { + unless ((rename($dot_loc, "$backup_dir/$dot_prefix")) + && (rename("$backup_dir/$dot_prefix.new", $dot_loc))) { + warn "[GpgEzmlm] failed to move base dotqmail file: $!"; + return undef; + } + } else { + warn "[GpgEzmlm] Warning: I expected a pristine base " + . "dotqmail file: $dot_loc"; + } + } else { + warn "[GpgEzmlm] failed to open base dotqmail file: $dot_loc"; + return undef; + } + close DOT_NEW; + } else { + warn "[GpgEzmlm] failed to create new base dotqmail file: " + . "$backup_dir/$dot_prefix.new"; + return undef; + } + + # move the "-default" dotqmail file + if (open(DEFAULT_NEW, ">$backup_dir/$dot_prefix-default.new")) { + if (open(DEFAULT_ORIG, "<$dot_loc-default")) { + my $line_found = (0==1); + while () { + my $line = $_; + if ($line =~ /ezmlm-manage\s+(\S+)/) { + print DEFAULT_NEW "\|$GPG_EZMLM_BASE/gpg-ezmlm-manage.pl $1\n"; + $line_found = (0==0); + } else { + print DEFAULT_NEW $line; + } + } + close DEFAULT_ORIG; + # move the original file to the backup and the new file back + if ($line_found) { + unless ((rename("$dot_loc-default", + "$backup_dir/$dot_prefix-default")) + && (rename("$backup_dir/$dot_prefix-default.new", + "$dot_loc-default"))) { + warn "[GpgEzmlm] failed to move default dotqmail file: $!"; + return undef; + } + } else { + warn "[GpgEzmlm] Warning: I expected a pristine default " + . "dotqmail file: $dot_loc-default"; + } + } else { + warn "[GpgEzmlm] failed to open default dotqmail file: " + . "$dot_loc-default"; + return undef; + } + close DEFAULT_NEW; + } else { + warn "[GpgEzmlm] failed to create new default dotqmail file: " + . "$backup_dir/$dot_prefix-default.new"; + return undef; + } + + return (0==0); +} + + +# activate the config file for encryption (gpg-ezmlm) +sub _enable_encryption_config_file { + my $list_dir = shift; + my ($backup_dir); + + $backup_dir = _get_config_backup_dir($list_dir); + + # check, if the current config file is for gpg-ezmlm or for ezmlm-idx + if (_is_encrypted($list_dir)) { + warn "[GpgEzmlm] I expected a pristine ezmlm-idx config file: " + . "$list_dir/config"; + return undef; + } + + # store the current original config file + if ((-e "$list_dir/config") && (!File::Copy::copy("$list_dir/config", + "$backup_dir/config.original"))) { + warn "[GpgEzmlm] failed to save the current ezmlm-idx config file ('" + . "$list_dir/config') to '$backup_dir/config.original': $!"; + return undef; + } + + # copy the encryption config file to the list directory + unless (File::Copy::copy("$backup_dir/config.gpg-ezmlm", + "$list_dir/config")) { + warn "[GpgEzmlm] failed to enable the gpg-ezmlm config file (from '" + . "$backup_dir/config.gpg-ezmlm' to '$list_dir/config'): $!"; + return undef; + } + + return (0==0); +} + + +# activate the config file for plain ezmlm-idx lists +sub _enable_plaintext_config_file { + my $list_dir = shift; + my ($backup_dir); + + $backup_dir = _get_config_backup_dir($list_dir); + + # check, if the current config file is for gpg-ezmlm or for ezmlm-idx + unless (_is_encrypted($list_dir)) { + warn "[GpgEzmlm] I expected a config file for gpg-ezmlm: " + . "$list_dir/config"; + return undef; + } + + # store the current gpg-ezmlm config file + unless (File::Copy::copy("$list_dir/config", + "$backup_dir/config.gpg-ezmlm")) { + warn "[GpgEzmlm] failed to save the current gpg-ezmlm config file ('" + . "$list_dir/config') to '$backup_dir/config.gpg-ezmlm': $!"; + return undef; + } + + # copy the ezmlm-idx config file to the list directory - or remove the + # currently active gpg-ezmlm config file + if (-e "$backup_dir/config.original") { + unless (File::Copy::copy("$backup_dir/config.original", + "$list_dir/config")) { + warn "[GpgEzmlm] failed to enable the originnal config file (from '" + . "$backup_dir/config.original' to '$list_dir/config': $!"; + return undef; + } + } else { + unless (unlink("$list_dir/config")) { + warn "[GpgEzmlm] failed to remove the gpg-ezmlm config file (" + . "$list_dir/config): $!"; + return undef; + } + } + + return (0==0); +} + + +# where should the dotqmail files and the config file be stored? +sub _get_config_backup_dir { + my $list_dir = shift; + return $list_dir . '/.gpg-ezmlm.backup'; +} + + +# == check version of gpg-ezmlm == +sub check_gpg_ezmlm_version { + my $ret_value = system("'$GPG_EZMLM_BASE/gpg-ezmlm-convert.pl' --version &>/dev/null"); + # for now we do not need a specific version of gpg-ezmlm - it just has to + # know the "--version" argument (available since gpg-ezmlm 0.3.4) + return ($ret_value == 0); +} + +# == check if gpg-ezmlm is installed == +sub is_available { + # the existence of the gpg-ezmlm script is sufficient for now + return -e "$GPG_EZMLM_BASE/gpg-ezmlm-convert.pl"; +} + +############ some internal functions ############## + +# == return an error message if appropriate == +sub errmsg { + my ($self) = @_; + return $self->{'ERRMSG'}; +} + +sub errno { + my ($self) = @_; + return $self->{'ERRNO'}; +} + + +# == Internal function to set the error to return == +sub _seterror { + my ($self, $no, $mesg) = @_; + + if (defined($no) && $no) { + if ($no < 0) { + $self->{'ERRNO'} = -1; + $self->{'ERRMSG'} = $mesg || 'An undefined error occoured'; + } else { + $self->{'ERRNO'} = $no / 256; + $self->{'ERRMSG'} = $! || $mesg || 'An undefined error occoured in a system() call'; + } + } else { + $self->{'ERRNO'} = 0; + $self->{'ERRMSG'} = undef; + } + return 1; +} + +1; + +=head1 AUTHOR + + Lars Kruse + +=head1 BUGS + + There are no known bugs. + + Please report bugs to the author or use the bug tracking system at + https://systemausfall.org/trac/ezmlm-web. + +=head1 SEE ALSO + + ezmlm(5), ezmlm-make(2), ezmlm-sub(1), + ezmlm-unsub(1), ezmlm-list(1), ezmlm-issub(1) + + https://systemausfall.org/toolforge/ezmlm-web/ + http://www.synacklabs.net/projects/crypt-ml/ + http://www.ezmlm.org/ + http://www.qmail.org/ + +=cut diff --git a/Ezmlm/tags/Ezmlm-0.08/Ezmlm/GpgKeyRing.pm b/Ezmlm/tags/Ezmlm-0.08/Ezmlm/GpgKeyRing.pm new file mode 100644 index 0000000..99ff26a --- /dev/null +++ b/Ezmlm/tags/Ezmlm-0.08/Ezmlm/GpgKeyRing.pm @@ -0,0 +1,399 @@ +# =========================================================================== +# Gpg.pm +# $Id$ +# +# Object methods for gpg-ezmlm mailing lists +# +# Copyright (C) 2006, Lars Kruse, All Rights Reserved. +# Please send bug reports and comments to devel@sumpfralle.de +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +# +# ========================================================================== + +package Mail::Ezmlm::GpgKeyRing; + +use strict; +use vars qw($GPG_BIN $VERSION @ISA @EXPORT @EXPORT_OK); +use Carp; +use Crypt::GPG; + +$VERSION = '0.1'; + +require 5.005; + +=head1 NAME + +Mail::Ezmlm::GpgKeyRing - Object Methods for gnupg keyring management + +=head1 SYNOPSIS + + use Mail::Ezmlm::GpgKeyRing; + $keyring = new Mail::Ezmlm::GpgKeyRing(DIRNAME); + +The rest is a bit complicated for a Synopsis, see the description. + +=head1 DESCRIPTION + +Mail::Ezmlm::GpgKeyRing is a Perl module that is designed to provide an object +interface to GnuPG keyrings for encrypted mailing lists. + +=cut + +# == Begin site dependant variables == +$GPG_BIN = '/usr/bin/gpg'; # Autoinserted by Makefile.PL + + +# == check the gpg path == +$GPG_BIN = '/usr/local/bin/gpg' + unless (-x "$GPG_BIN"); +$GPG_BIN = '/usr/bin/gpg' + unless (-x "$GPG_BIN"); +$GPG_BIN = '/bin/gpg' + unless (-x "$GPG_BIN"); +$GPG_BIN = '/usr/local/bin/gpg2' + unless (-x "$GPG_BIN"); +$GPG_BIN = '/usr/bin/gpg2' + unless (-x "$GPG_BIN"); +$GPG_BIN = '/bin/gpg2' + unless (-x "$GPG_BIN"); + +# == clean up the path == +local $ENV{'PATH'} = "/bin"; + +# check, if gpg is installed +unless (-x "$GPG_BIN") { + die("Warning: gnupg does not seem to be installed - none of the " + . "executables 'gpg' or 'gpg2' were found at the usual locations!"); +} + + +# == Initialiser - Returns a reference to the object == + +=head2 Setting up a new Mail::Ezmlm::GpgKeyRing object: + + use Mail::Ezmlm::GpgKeyRing; + $keyring = new Mail::Ezmlm::GpgKeyRing('/home/user/lists/foolist/.gnupg'); + +new() returns the new instance for success, undefined if there was a problem. + +=cut + +sub new { + my($class, $keyring_dir) = @_; + my $self = {}; + bless $self, ref $class || $class || 'Mail::Ezmlm::GpgKeyRing'; + if ($self->set_location($keyring_dir)) { + return $self; + } else { + return undef; + } +} + + +# == Return the directory of the gnupg keyring == + +=head2 Determining the location of the configured keyring. + + $whichkeyring = $keyring->get_location(); + print $keyring->get_location(); + +=cut + +sub get_location { + my($self) = shift; + return $self->{'KEYRING_DIR'}; +} + + +# == Set the current keyring directory == + +=head2 Changing which keyring the Mail::Ezmlm::GpgKeyRing object points at: + + $keyring->set_location('/home/user/lists/foolist/.gnupg'); + +=cut + +sub set_location { + my($self, $keyring_dir) = @_; + if (-e "$keyring_dir") { + if (-x "$keyring_dir") { + # at least it is a directory - so it looks ok + $self->{'KEYRING_DIR'} = $keyring_dir; + } else { + # it seems to be a file or something else - we complain + warn "GPG keyring location must be a directory: $keyring_dir"; + $self->{'KEYRING_DIR'} = undef; + } + } else { + # probably the keyring directory does not exist, yet + # a warning should not be necessary + $self->{'KEYRING_DIR'} = $keyring_dir; + } + return $self->{'KEYRING_DIR'} +} + + +# == export a key == + +=head2 Export a key: + +You may export public keys of the keyring. + +The key can be identified by its id or other (unique) patterns (like the +gnupg program). + + $keyring->export_key($key_id); + $keyring->export_key($email_address); + +The return value is a string containing the ascii armored key data. + +=cut + +sub export_key { + my ($self, $keyid) = @_; + my ($gpg, $gpgoption, $gpgcommand, $output); + + # return immediately - this avoids creating an empty keyring unintentionally + return () unless (-e $self->{'KEYRING_DIR'}); + $gpg = $self->_get_gpg_object(); + $gpgoption = "--armor --export $keyid"; + $gpgcommand = $gpg->gpgbin() . " " . $gpg->gpgopts() . " $gpgoption"; + $output = `$gpgcommand 2>/dev/null`; + if ($output) { + return $output; + } else { + return undef; + } +} + + +# == import a new key == + +=head2 Import a key: + +You can import public or secret keys into the keyring. + +The key should be ascii armored. + + $keyring->import_key($ascii_armored_key_data); + +=cut + +sub import_key { + my ($self, $key) = @_; + my $gpg = $self->_get_gpg_object(); + if ($gpg->addkey($key)) { + return (0==0); + } else { + return (1==0); + } +} + + +# == delete a key == + +=head2 Delete a key: + +Remove a public key (and the matching secret key if it exists) from the keyring. + +The argument is the id of the key or any other unique pattern. + + $keyring->delete_key($keyid); + +=cut + +sub delete_key { + my ($self, $keyid) = @_; + my $gpg = $self->_get_gpg_object(); + my $fprint = $self->_get_fingerprint($keyid); + return (1==0) unless (defined($fprint)); + my $gpgoption = "--delete-secret-and-public-key $fprint"; + my $gpgcommand = $gpg->gpgbin() . " " . $gpg->gpgopts() . " $gpgoption"; + if (system($gpgcommand)) { + return (1==0); + } else { + return (0==0); + } +} + + +# == generate new private key == + +=head2 Generate a new key: + + $keyring->generate_key($name, $comment, $email_address, $keysize, $expire); + +Refer to the documentation of gnupg for the format of the arguments. + +=cut + +sub generate_private_key { + my ($self, $name, $comment, $email, $keysize, $expire) = @_; + my $gpg = $self->_get_gpg_object(); + my $gpgoption = "--gen-key"; + my $gpgcommand = $gpg->gpgbin() . " " . $gpg->gpgopts() . " $gpgoption"; + my $pid = open(INPUT, "| $gpgcommand"); + print INPUT "Key-Type: DSA\n"; + print INPUT "Key-Length: 1024\n"; + print INPUT "Subkey-Type: ELG-E\n"; + print INPUT "Subkey-Length: $keysize\n"; + print INPUT "Name-Real: $name\n"; + print INPUT "Name-Comment: $comment\n" if ($comment); + print INPUT "Name-Email: $email\n"; + print INPUT "Expire-Date: $expire\n"; + return close INPUT; +} + + +# == get_public_keys == + +=head2 Getting public keys: + +Return an array of key hashes each containing the following elements: + +=over + +=item * +name + +=item * +email + +=item * +id + +=item * +expires + +=back + + $keyring->get_public_keys(); + $keyring->get_secret_keys(); + +=cut + +sub get_public_keys { + my ($self) = @_; + my @keys = $self->_get_keys("pub"); + return @keys; +} + + +# == get_private_keys == +# see above for POD (get_public_keys) +sub get_secret_keys { + my ($self) = @_; + my @keys = $self->_get_keys("sec"); + return @keys; +} + + +############ some internal functions ############## + +# == internal function for creating a gpg object == +sub _get_gpg_object() { + my ($self) = @_; + my $gpg = new Crypt::GPG(); + my $dirname = $self->get_location(); + # replace whitespace characters in the keyring directory name + $dirname =~ s/(\s)/\\$1/g; + $gpg->gpgbin($GPG_BIN); + $gpg->gpgopts("--lock-multiple --no-tty --no-secmem-warning --batch --quiet --homedir $dirname"); + return $gpg; +} + + +# == internal function to list keys == +sub _get_keys() { + # type can be "pub" or "sec" + my ($self, $keyType) = @_; + my ($gpg, $flag, $gpgoption, @keys, $key); + + # return immediately - this avoids creating an empty keyring unintentionally + return () unless (-r $self->{'KEYRING_DIR'}); + $gpg = $self->_get_gpg_object(); + if ($keyType eq "pub") { + $flag = "pub"; + $gpgoption = "--list-keys"; + } elsif ($keyType eq "sec") { + $flag = "sec"; + $gpgoption = "--list-secret-keys"; + } else { + warn "wrong keyType: $keyType"; + return undef; + } + my $gpgcommand = $gpg->gpgbin() . " " . $gpg->gpgopts() . " --with-colons $gpgoption"; + my @read_keys = grep /^$flag/, `$gpgcommand`; + foreach $key (@read_keys) { + my ($type, $trust, $size, $algorithm, $id, $created, + $expires, $u2, $ownertrust, $uid) = split ":", $key; + # stupid way of "decoding" utf8 (at least it works for ":") + $uid =~ s/\\x3a/:/g; + $uid =~ /^(.*) <([^<]*)>/; + my $name = $1; + my $email = $2; + push @keys, {name => $name, email => $email, id => $id, expires => $expires}; + } + return @keys; +} + + +# == internal function to retrieve the fingerprint of a key == +sub _get_fingerprint() +{ + my ($self, $key_id) = @_; + my $gpg = $self->_get_gpg_object(); + $key_id =~ /^([0-9A-Z]*)$/; + $key_id = $1; + return undef unless ($key_id); + my $gpgoption = "--fingerprint $key_id"; + + my $gpgcommand = $gpg->gpgbin() . " " . $gpg->gpgopts() . " --with-colons $gpgoption"; + + my @fingerprints = grep /^fpr:/, `$gpgcommand`; + if (@fingerprints > 1) { + warn "[Mail::Ezmlm::GpgKeyRing] more than one key matched ($key_id)!"; + return undef; + } + return undef if (@fingerprints < 1); + my $fpr = $fingerprints[0]; + $fpr =~ /^fpr:*([0-9A-Z]*):*$/; + $fpr = $1; + return undef unless $1; + return $1; +} + + +=head1 AUTHOR + + Lars Kruse + +=head1 BUGS + + There are no known bugs. + + Please report bugs to the author or use the bug tracking system at + https://systemausfall.org/trac/ezmlm-web. + +=head1 SEE ALSO + + gnupg(7), gpg(1), gpg2(1), Crypt::GPG(3pm) + + https://systemausfall.org/toolforge/ezmlm-web/ + http://www.ezmlm.org/ + +=cut + diff --git a/Ezmlm/tags/Ezmlm-0.08/MANIFEST b/Ezmlm/tags/Ezmlm-0.08/MANIFEST new file mode 100644 index 0000000..3bd323b --- /dev/null +++ b/Ezmlm/tags/Ezmlm-0.08/MANIFEST @@ -0,0 +1,9 @@ +Changes +Ezmlm.pm +MANIFEST +README +Makefile.PL +test.pl +META.yml +Ezmlm/GpgKeyRing.pm +Ezmlm/GpgEzmlm.pm diff --git a/Ezmlm/tags/Ezmlm-0.08/META.yml b/Ezmlm/tags/Ezmlm-0.08/META.yml new file mode 100644 index 0000000..5fa8fbb --- /dev/null +++ b/Ezmlm/tags/Ezmlm-0.08/META.yml @@ -0,0 +1,10 @@ +# http://module-build.sourceforge.net/META-spec.html +#XXXXXXX This is a prototype!!! It will change in the future!!! XXXXX# +name: Ezmlm +version: 0.08 +version_from: Ezmlm.pm +installdirs: site +requires: + +distribution_type: module +generated_by: ExtUtils::MakeMaker version 6.17 diff --git a/Ezmlm/tags/Ezmlm-0.08/Makefile.PL b/Ezmlm/tags/Ezmlm-0.08/Makefile.PL new file mode 100644 index 0000000..b543bdf --- /dev/null +++ b/Ezmlm/tags/Ezmlm-0.08/Makefile.PL @@ -0,0 +1,232 @@ +# $Id: Makefile.PL,v 1.3 2005/03/05 14:15:20 guy Exp $ + +use ExtUtils::MakeMaker; +# See lib/ExtUtils/MakeMaker.pm for details of how to influence +# the contents of the Makefile that is written. +WriteMakefile( + 'CONFIGURE' => \&set_paths, + 'NAME' => 'Mail::Ezmlm', + 'VERSION_FROM' => 'Ezmlm.pm', # finds $VERSION + 'PREREQ_PM' => { 'File::Copy' => 0, 'Crypt::GPG' => 0 }, + 'DISTNAME' => 'Ezmlm', + 'dist' => { COMPRESS => 'gzip', SUFFIX => 'gz' }, + 'clean' => { FILES => 'ezmlmtmp' } +); + +sub set_paths { + my ($qmail_path, $ezmlm_path, $gpg_ezmlm_path, $gpg_ezmlm_requested); + my ($gpg_bin, $gpg_bin_requested); + + # special case to handle the FreeBSD ports system + if ($ENV{BSD_BATCH_INSTALL}) { + print STDERR "\$BSD_BATCH_INSTALL is set in your environment, assuming port defaults\n"; + return {}; + } + + print << 'EOM'; + +We now need to know where some things live on your system. I'll try and make +some intelligent guesses - if I get it right, please just press enter at the +prompt. If I get them wrong, please type in the correct path for me and then +press enter. + +First I need to know where the Ezmlm binaries live (ie where I can find +ezmlm-make, ezmlm-sub, etc). + +EOM + + *prompt = \&ExtUtils::MakeMaker::prompt; + + # guess default + $ezmlm_path = '/usr/local/bin/ezmlm'; + $ezmlm_path = '/usr/local/bin/ezmlm-idx' unless (-e "$ezmlm_path/ezmlm-make"); + $ezmlm_path = '/usr/local/bin' unless (-e "$ezmlm_path/ezmlm-make"); + $ezmlm_path = '/usr/bin/ezmlm' unless (-e "$ezmlm_path/ezmlm-make"); + $ezmlm_path = '/usr/bin/ezmlm-idx' unless (-e "$ezmlm_path/ezmlm-make"); + $ezmlm_path = '/usr/bin' unless (-e "$ezmlm_path/ezmlm-make"); + # return to default, if nothing can be found + $ezmlm_path = '/usr/local/bin/ezmlm' unless (-e "$ezmlm_path/ezmlm-make"); + + foreach (1..10) { + $ezmlm_path = prompt('Ezmlm binary directory?', "$ezmlm_path"); + last if (-e "$ezmlm_path/ezmlm-make"); + print "I can't find $ezmlm_path/ezmlm-make. Please try again\n"; + } + unless (-e "$ezmlm_path/ezmlm-make") { + print STDERR "Warning: No correct input after $_ attempts. Continuing with warnings ...\n"; + } + unless (system(("$ezmlm_path/ezmlm-make", "-V")) == 0) { + print STDERR "Warning: your version of ezmlm-make does not support the '-V' argument. Please upgrade to ezmlm-idx v0.400 or above.\n"; + } + + + print << 'EOM'; + +Now I need to know where Qmail resides on your system. The Qmail base +directory is the one in which the Qmail bin, control, etc directories +live in. + +EOM + + foreach (1..10) { + $qmail_path = prompt('Qmail base directory?', '/var/qmail'); + last if (-e "$qmail_path/control"); + print "I can't find $qmail_path/control. Please try again\n"; + } + if (! -e "$qmail_path/control") { + print STDERR "Warning: No correct input after $_ attempts. Continuing with warnings ...\n"; + } + + + # check if gpg-ezmlm is installed (for Mail::Ezmlm::GpgEzmlm) + $gpg_ezmlm_requested = prompt('Is gpg-ezmlm installed for encrypted mailing list support? (y/N)', "n"); + $gpg_ezmlm_requested = ($gpg_ezmlm_requested =~ /^y/i); + if ($gpg_ezmlm_requested) { + undef $gpg_ezmlm_path; + foreach ('/usr/local/bin', '/usr/bin', '/usr/local/bin/gpg-ezmlm', + '/usr/bin/gpg-ezmlm') { + if (-e "$_/gpg-ezmlm-manage.pl") { + $gpg_ezmlm_path = $_; + last; + } + } + $gpg_ezmlm_path = '/usr/bin' unless (defined($gpg_ezmlm_path)); + # ask the user to confirm our guessing + foreach (1..10) { + $gpg_ezmlm_path = prompt('gpg-ezmlm installation directory?', + "$gpg_ezmlm_path"); + last if (-e "$gpg_ezmlm_path/gpg-ezmlm-manage.pl"); + print "I can't find $gpg_ezmlm_path/gpg-ezmlm-manage.pl. " + . "Please try again\n"; + } + unless (-e "$gpg_ezmlm_path/gpg-ezmlm-manage.pl") { + print STDERR "Warning: No correct input after $_ attempts. " + . "Continuing with warnings ...\n"; + } + } + + # check if gpg is installed (for Mail::Ezmlm::GpgKeyRing) + $gpg_bin_requested = prompt('Is gnupg installed (for keyring support in encrypted mailing lists)? (y/N)', "n"); + $gpg_bin_requested = ($gpg_bin_requested =~ /^y/i); + if ($gpg_bin_requested) { + undef $gpg_bin; + foreach ('/usr/local/bin/gpg', '/usr/bin/gpg', '/bin/gpg', + '/usr/local/bin/gpg2', '/usr/bin/gpg2', '/bin/gpg2') { + if (-x "$_") { + $gpg_bin = $_; + last; + } + } + $gpg_bin = '/usr/bin' unless (defined($gpg_bin)); + # ask the user to confirm our guessing + foreach (1..10) { + $gpg_bin = prompt('Path to the gpg or gpg2 binary?', "$gpg_bin"); + last if (-x "$gpg_bin"); + print "I can't find $gpg_bin. Please try again\n"; + } + unless (-x "$gpg_bin") { + print STDERR "Warning: No correct input after $_ attempts. " + . "Continuing with warnings ...\n"; + } + } + + # check if mysql support is necessary + if(`strings $ezmlm_path/ezmlm-sub | grep -i 'MySQL'`) { + + print << 'EOM'; + +It appears you have compiled MySQL support into your version of Ezmlm. If +this is correct, I now need to know where the MySQL client (mysql) lives on +your machine. + +Please leave this blank if you do not want to enable MySQL support in the +Mail::Ezmlm module. + +EOM + + $mysql_path = '/usr/bin'; + $mysql_path = '/usr/local/bin' unless (-e "$mysql_path/mysql"); + # return to default - if nothing works + $mysql_path = '/usr/bin' unless (-e "$mysql_path/mysql"); + + foreach (1..10) { + $mysql_path = prompt('MySQL binary directory?', "$mysql_path"); + last if (-e "$mysql_path/mysql" || $mysql_path eq ''); + print "I can't find $mysql_path/mysql. Please enter the full path\n"; + print "or leave this option blank if you don't want to use MySQL\n"; + } + unless ((-e "$mysql_path/mysql") || ($mysql_path eq '')) { + print STDERR "Warning: No correct input after $_ attempts. Continuing with warnings ...\n"; + } + + } + + print << 'EOM'; + +Thank you. I will use this information to configure Mail::Ezmlm for you + +EOM + + # set the variables in Ezmlm.pm + # Back up file + open(EZMLM, 'Ezmlm.pm.tmp.$$") or die "Unable to create temp file: $!"; + while() { print TMP; } + close TMP; close EZMLM; + # Do variable substitution + open(EZMLM, '>Ezmlm.pm') or die "Unable to open Ezmlm.pm for write: $!"; + open(TMP, ") { + s{^\$EZMLM_BASE\s*=\s*['"].+?['"]\s*;\s*(#.*|)$}{\$EZMLM_BASE = '$ezmlm_path'; #Autoinserted by Makefile.PL}; + s{^\$QMAIL_BASE\s*=\s*['"].+?['"]\s*;\s*(#.*|)$}{\$QMAIL_BASE = '$qmail_path'; #Autoinserted by Makefile.PL}; + s{^\$MYSQL_BASE\s*=\s*['"].*?['"]\s*;\s*(#.*|)$}{\$MYSQL_BASE = '$mysql_path'; #Autoinserted by Makefile.PL}; + print EZMLM; + } + close TMP; close EZMLM; + unlink "Ezmlm.pm.tmp.$$"; + + if ($gpg_ezmlm_requested) { + # set the variables in GpgEzmlm.pm + # Back up file + open(GPGEZMLM, 'Ezmlm/GpgEzmlm.pm.tmp.$$") or die "Unable to create temp file: $!"; + while() { print TMP; } + close TMP; close GPGEZMLM; + # Do variable substitution + open(GPGEZMLM, '>Ezmlm/GpgEzmlm.pm') + or die "Unable to open Ezmlm/GpgEzmlm.pm for write: $!"; + open(TMP, ") { + s{^\$GPG_EZMLM_BASE\s*=\s*['"].+?['"]\s*;\s*(#.*|)$}{\$GPG_EZMLM_BASE = '$gpg_ezmlm_path'; # Autoinserted by Makefile.PL}; + s{^\$GPG_BIN\s*=\s*['"].+?['"]\s*;\s*(#.*|)$}{\$GPG_BIN = '$gpg_bin'; # Autoinserted by Makefile.PL} if ($gpg_bin_requested); + print GPGEZMLM; + } + close TMP; close GPGEZMLM; + unlink "Ezmlm/GpgEzmlm.pm.tmp.$$"; + } + + # set the variables in GpgKeyRing.pm + if ($gpg_bin_requested) { + # Back up file + open(GPGKEYRING, 'Ezmlm/GpgKeyRing.pm.tmp.$$") or die "Unable to create temp file: $!"; + while() { print TMP; } + close TMP; close GPGKEYRING; + # Do variable substitution + open(GPGKEYRING, '>Ezmlm/GpgKeyRing.pm') or die "Unable to open Ezmlm/GpgKeyRing.pm for write: $!"; + open(TMP, ") { + s{^\$GPG_BIN\s*=\s*['"].+?['"]\s*;\s*(#.*|)$}{\$GPG_BIN = '$gpg_bin'; # Autoinserted by Makefile.PL}; + print GPGKEYRING; + } + close TMP; close GPGKEYRING; + unlink "Ezmlm/GpgKeyRing.pm.tmp.$$"; + } + + return {}; + +} + diff --git a/Ezmlm/tags/Ezmlm-0.08/README b/Ezmlm/tags/Ezmlm-0.08/README new file mode 100644 index 0000000..68f6e16 --- /dev/null +++ b/Ezmlm/tags/Ezmlm-0.08/README @@ -0,0 +1,19 @@ +$Id$ + +Ezmlm.pm + +Object methods for ezmlm mailing lists. + +Install by doing the following ... +# perl Makefile.PL +# make test +# make install + +One thing. For some reason MakeMaker doesn't like symlinks. Please make sure +you use the full canonical path for the qmail and ezmlm binaries. + +Documentation is in pod format. Please run perldoc Mail::Ezmlm after you have +installed it. + +- Guy Antony Halse +- Lars Kruse diff --git a/Ezmlm/tags/Ezmlm-0.08/test.pl b/Ezmlm/tags/Ezmlm-0.08/test.pl new file mode 100644 index 0000000..4e0d4ee --- /dev/null +++ b/Ezmlm/tags/Ezmlm-0.08/test.pl @@ -0,0 +1,234 @@ +# =========================================================================== +# test.pl - version 0.02 - 25/09/2000 +# $Id: test.pl,v 1.5 2005/03/05 14:08:30 guy Exp $ +# Test suite for Mail::Ezmlm +# +# Copyright (C) 1999, Guy Antony Halse, All Rights Reserved. +# Please send bug reports and comments to guy-ezmlm@rucus.ru.ac.za +# +# This program is subject to the restrictions set out in the copyright +# agreement that can be found in the Ezmlm.pm file in this distribution +# +# ========================================================================== +# Before `make install' is performed this script should be runnable with +# `make test'. After `make install' it should work as `perl test.pl' + +######################### We start with some black magic to print on failure. + +$failed = 0; + +BEGIN { $| = 1; print "1..9\n"; } +END {($failed++ && print "not ok 1\n") unless $loaded;} +use Mail::Ezmlm; +$loaded = 1; +print "Loading: ok 1\n"; + +######################### End of black magic. + +# Insert your test code below (better if it prints "ok 13" +# (correspondingly "not ok 13") depending on the success of chunk 13 +# of the test code): + +use Cwd; +use File::Find; +$list = new Mail::Ezmlm; + +# create a temp directory if necessary +$TMP = cwd() . '/ezmlmtmp'; +mkdir $TMP, 0755 unless (-d $TMP); + +print 'Checking list creation: '; +$test1 = $list->make(-name=>"ezmlm-test1-$$", + -qmail=>"$TMP/.qmail-ezmlm-test1-$$", + -dir=>"$TMP/ezmlm-test1-$$"); +if($test1 eq "$TMP/ezmlm-test1-$$") { + print "ok 2\n"; +} else { + print 'not ok 2 [', $list->errmsg(), "]\n"; + $failed++; +} + +print 'Checking vhost list creation: '; +$test2 = $list->make(-name=>"ezmlm-test2-$$", + -qmail=>"$TMP/.qmail-ezmlm-test2-$$", + -dir=>"$TMP/ezmlm-test2-$$", + -host=>'on.web.za', + -user=>'onwebza'); +if($test2 eq "$TMP/ezmlm-test2-$$") { + open(INLOCAL, "<$TMP/ezmlm-test2-$$/inlocal"); + chomp($test2 = ); + close INLOCAL; + if($test2 eq "onwebza-ezmlm-test2-$$") { + print "ok 3\n"; + } else { + print 'not ok 3 [', $list->errmsg(), "]\n"; + $failed++; + } +} else { + print 'not ok 3 [', $list->errmsg(), "]\n"; + $failed++; +} + +print 'Testing list update: '; +if($list->update('ms')) { + print "ok 4\n"; +} else { + print 'not ok 4 [', $list->errmsg(), "]\n"; + $failed++; +} + +print 'Testing setlist() and thislist(): '; +$list->setlist("$TMP/ezmlm-test1-$$"); +if($list->thislist eq "$TMP/ezmlm-test1-$$") { + print "ok 5\n"; +} else { + print 'not ok 5 [', $list->errmsg(), "]\n"; + $failed++; +} + +print 'Testing list subscription and subscription listing: '; +$list->sub('nobody@on.web.za'); +$list->sub('anonymous@on.web.za', 'test@on.web.za'); +@subscribers = $list->subscribers; +if($subscribers[1] =~ /nobody\@on.web.za/) { + print "ok 6\n"; +} else { + print 'not ok 6 [', $list->errmsg(), "]\n"; + $failed++; +} + +print 'Testing issub(): '; +if(defined($list->issub('nobody@on.web.za'))) { + if(defined($list->issub('some@non.existant.address'))) { + print 'not ok 7 [', $list->errmsg(), "]\n"; + $failed++; + } else { + print "ok 7\n"; + } +} else { + print 'not ok 7 [', $list->errmsg(), "]\n"; + $failed++; +} + +print 'Testing list unsubscription: '; +$list->unsub('nobody@on.web.za'); +$list->unsub('anonymous@on.web.za', 'test@on.web.za'); +@subscribers = $list->subscribers; +unless(@subscribers) { + print "ok 8\n"; +} else { + print 'not ok 8 [', $list->errmsg(), "]\n"; + $failed++; +} + +print 'Testing installed version of ezmlm: '; +my($version) = $list->check_version(); +if ($version) { + $version =~ s/\n//; + print 'not ok 9 [Warning: Ezmlm.pm is designed to work with ezmlm-idx > 0.40. Your version reports as: ', $version, "]\n"; +} else { + print "ok 9\n"; +} + +print 'Testing retrieving of text files: '; +if ($list->get_text_content('sub-ok') ne '') { + print "ok 10\n"; +} else { + print 'not ok 10 [', $list->errmsg(), "]\n"; + $failed++; +} + +print 'Testing changing of text files: '; +$list->set_text_content('sub-ok', "testing message\n"); +if ($list->get_text_content('sub-ok') eq "testing message\n") { + print "ok 11\n"; +} else { + print 'not ok 11 [', $list->errmsg(), "]\n"; + $failed++; +} + +print 'Testing if text file is marked as customized (only idx >= 5.0): '; +if ($list->get_version() >= 5) { + if ($list->is_text_default('sub-ok')) { + print 'not ok 12 [', $list->errmsg(), "]\n"; + $failed++; + } else { + print "ok 12\n"; + } +} else { + print "ok 12 [skipped]\n"; +} + +print 'Testing resetting text files (only idx >= 5.0): '; +if ($list->get_version() >= 5) { + $list->reset_text('sub-ok'); + if ($list->is_text_default('sub-ok')) { + print "ok 13\n"; + } else { + print 'not ok 13 [', $list->errmsg(), "]\n"; + $failed++; + } +} else { + print "ok 13 [skipped]\n"; +} + +print 'Testing retrieving available languages (only idx >= 5.0): '; +if ($list->get_version() >= 5) { + my @avail_langs = $list->get_available_languages(); + if ($#avail_langs > 0) { + print "ok 14\n"; + } else { + print 'not ok 14 [', $list->errmsg(), "]\n"; + $failed++; + } +} else { + print "ok 14 [skipped]\n"; +} + +print 'Testing changing the configured language (only idx >= 5.0): '; +if ($list->get_version() >= 5) { + my @avail_langs = $list->get_available_languages(); + $list->set_lang($avail_langs[$#avail_langs-1]); + if ($list->get_lang() eq $avail_langs[$#avail_langs-1]) { + print "ok 15\n"; + } else { + print 'not ok 15 [', $list->errmsg(), "]\n"; + $failed++; + } +} else { + print "ok 15 [skipped]\n"; +} + +print 'Testing getting the configuration directory (only idx >= 5.0): '; +if ($list->get_version() >= 5) { + if ($list->get_config_dir() ne '') { + print "ok 16\n"; + } else { + print 'not ok 16 [', $list->errmsg(), "]\n"; + $failed++; + } +} else { + print "ok 16 [skipped]\n"; +} + +print 'Testing changing the configuration directory (only idx >= 5.0): '; +if ($list->get_version() >= 5) { + $list->set_config_dir('/etc/ezmlm-local'); + if ($list->get_config_dir() eq '/etc/ezmlm-local') { + print "ok 17\n"; + } else { + print 'not ok 17 [', $list->errmsg(), "]\n"; + $failed++; + } +} else { + print "ok 17 [skipped]\n"; +} + +if($failed > 0) { + print "\n$failed tests were failed\n"; + exit $failed; +} else { + print "\nSuccessful :-)\n"; + finddepth(sub { (-d $File::Find::name) ? rmdir ($File::Find::name) : unlink ($File::Find::name) }, cwd() . "/ezmlmtmp"); + exit; +} diff --git a/Ezmlm/tags/packages/Ezmlm-0.08.tar.gz b/Ezmlm/tags/packages/Ezmlm-0.08.tar.gz new file mode 100644 index 0000000000000000000000000000000000000000..6b9f652bddb600159cdc609168cf390a6b6c9222 GIT binary patch literal 25039 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
  • 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 literal 0 HcmV?d00001 diff --git a/Ezmlm/tags/packages/libemail-ezmlm-perl_0.08-1_all.deb b/Ezmlm/tags/packages/libemail-ezmlm-perl_0.08-1_all.deb new file mode 100644 index 0000000000000000000000000000000000000000..13814a162d5247f7c10e3f412ff72f962a7af5a9 GIT binary patch literal 36828 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 literal 0 HcmV?d00001