From 01865d2ba4b5d1278a44a91654a65b24327ce6bb Mon Sep 17 00:00:00 2001 From: lars Date: Thu, 16 Oct 2008 01:34:44 +0000 Subject: [PATCH] Mail::Ezmlm release v0.08.2: * updated debian files * tagged release * uploaded packages --- Ezmlm/debian/changelog | 6 + Ezmlm/debian/control | 2 +- Ezmlm/tags/Ezmlm-0.08.2/Changes | 64 + Ezmlm/tags/Ezmlm-0.08.2/Ezmlm.pm | 1236 +++++++++++++++++ Ezmlm/tags/Ezmlm-0.08.2/Ezmlm/GpgEzmlm.pm | 887 ++++++++++++ Ezmlm/tags/Ezmlm-0.08.2/Ezmlm/GpgKeyRing.pm | 399 ++++++ Ezmlm/tags/Ezmlm-0.08.2/MANIFEST | 9 + Ezmlm/tags/Ezmlm-0.08.2/META.yml | 10 + Ezmlm/tags/Ezmlm-0.08.2/Makefile.PL | 232 ++++ Ezmlm/tags/Ezmlm-0.08.2/README | 19 + Ezmlm/tags/Ezmlm-0.08.2/test.pl | 255 ++++ Ezmlm/tags/packages/Ezmlm-0.08.2.tar.gz | Bin 0 -> 25380 bytes .../libemail-ezmlm-perl_0.08.2-1_all.deb | Bin 0 -> 37034 bytes 13 files changed, 3118 insertions(+), 1 deletion(-) create mode 100644 Ezmlm/tags/Ezmlm-0.08.2/Changes create mode 100644 Ezmlm/tags/Ezmlm-0.08.2/Ezmlm.pm create mode 100644 Ezmlm/tags/Ezmlm-0.08.2/Ezmlm/GpgEzmlm.pm create mode 100644 Ezmlm/tags/Ezmlm-0.08.2/Ezmlm/GpgKeyRing.pm create mode 100644 Ezmlm/tags/Ezmlm-0.08.2/MANIFEST create mode 100644 Ezmlm/tags/Ezmlm-0.08.2/META.yml create mode 100644 Ezmlm/tags/Ezmlm-0.08.2/Makefile.PL create mode 100644 Ezmlm/tags/Ezmlm-0.08.2/README create mode 100644 Ezmlm/tags/Ezmlm-0.08.2/test.pl create mode 100644 Ezmlm/tags/packages/Ezmlm-0.08.2.tar.gz create mode 100644 Ezmlm/tags/packages/libemail-ezmlm-perl_0.08.2-1_all.deb diff --git a/Ezmlm/debian/changelog b/Ezmlm/debian/changelog index 01b1b50..95ac9bf 100644 --- a/Ezmlm/debian/changelog +++ b/Ezmlm/debian/changelog @@ -1,3 +1,9 @@ +libemail-ezmlm-perl (0.08.2-1) unstable; urgency=low + + * New upstream release + + -- Lars Kruse Thu, 16 Oct 2008 03:17:42 +0200 + libemail-ezmlm-perl (0.08.1-1) unstable; urgency=low * New upstream release diff --git a/Ezmlm/debian/control b/Ezmlm/debian/control index b87749c..d68f65e 100644 --- a/Ezmlm/debian/control +++ b/Ezmlm/debian/control @@ -9,7 +9,7 @@ Homepage: https://systemausfall.org/toolforge/ezmlm-web Package: libemail-ezmlm-perl Section: perl Architecture: all -Depends: ${perl:Depends}, libcrypt-gpg-perl +Depends: ${perl:Depends}, libcrypt-gpg-perl, gnupg Description: access ezmlm-idx mailig lists with perl's object methods The support for ezmlm-idx 6.0 is complete. The module still works fine with ezmlm-idx 0.4xx, too. diff --git a/Ezmlm/tags/Ezmlm-0.08.2/Changes b/Ezmlm/tags/Ezmlm-0.08.2/Changes new file mode 100644 index 0000000..e82eb6a --- /dev/null +++ b/Ezmlm/tags/Ezmlm-0.08.2/Changes @@ -0,0 +1,64 @@ +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 + +0.08.1 Thu Oct 12 04:37:06 CEST 2008 + - fixed issues of Mail::Ezmlm::GpgEzmlm with ezmlm-idx v0.4x lists + +0.08.2 Wed Oct 15 23:00:12 CEST 2008 + - added check for external dependency to the test script + diff --git a/Ezmlm/tags/Ezmlm-0.08.2/Ezmlm.pm b/Ezmlm/tags/Ezmlm-0.08.2/Ezmlm.pm new file mode 100644 index 0000000..98b2100 --- /dev/null +++ b/Ezmlm/tags/Ezmlm-0.08.2/Ezmlm.pm @@ -0,0 +1,1236 @@ +# =========================================================================== +# Ezmlm.pm - version 0.08.2 - 10/15/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.2'; + +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 == +# return 0 for a valid version +# return the version string for an invalid version +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.2/Ezmlm/GpgEzmlm.pm b/Ezmlm/tags/Ezmlm-0.08.2/Ezmlm/GpgEzmlm.pm new file mode 100644 index 0000000..1540ad8 --- /dev/null +++ b/Ezmlm/tags/Ezmlm-0.08.2/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") =~ /^T:(.*)$/m); + } 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.2/Ezmlm/GpgKeyRing.pm b/Ezmlm/tags/Ezmlm-0.08.2/Ezmlm/GpgKeyRing.pm new file mode 100644 index 0000000..99ff26a --- /dev/null +++ b/Ezmlm/tags/Ezmlm-0.08.2/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.2/MANIFEST b/Ezmlm/tags/Ezmlm-0.08.2/MANIFEST new file mode 100644 index 0000000..3bd323b --- /dev/null +++ b/Ezmlm/tags/Ezmlm-0.08.2/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.2/META.yml b/Ezmlm/tags/Ezmlm-0.08.2/META.yml new file mode 100644 index 0000000..5fa8fbb --- /dev/null +++ b/Ezmlm/tags/Ezmlm-0.08.2/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.2/Makefile.PL b/Ezmlm/tags/Ezmlm-0.08.2/Makefile.PL new file mode 100644 index 0000000..b543bdf --- /dev/null +++ b/Ezmlm/tags/Ezmlm-0.08.2/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.2/README b/Ezmlm/tags/Ezmlm-0.08.2/README new file mode 100644 index 0000000..68f6e16 --- /dev/null +++ b/Ezmlm/tags/Ezmlm-0.08.2/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.2/test.pl b/Ezmlm/tags/Ezmlm-0.08.2/test.pl new file mode 100644 index 0000000..2b574d2 --- /dev/null +++ b/Ezmlm/tags/Ezmlm-0.08.2/test.pl @@ -0,0 +1,255 @@ +# =========================================================================== +# 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; + +# we need to check, if ezmlm-idx is installed +sub check_external_dependency { + my $ezmlm_bin = $Mail::Ezmlm::EZMLM_BASE . "/ezmlm-make"; + if (-x $ezmlm_bin) { + return (0==0); + } else { + return (0==1); + } +} + +if (!check_external_dependency()) { + # For humans: + warn "You don't have ezmlm-idx installed. Please fetch it from " + . "http://ezmlm.org/ and install it."; + # For the tester scripts: + exit 0; +} + +$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(); +# "check_version" returns zero for a valid version and the version string for an +# invalid version of ezmlm +if ($version != 0) { + $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.2.tar.gz b/Ezmlm/tags/packages/Ezmlm-0.08.2.tar.gz new file mode 100644 index 0000000000000000000000000000000000000000..1e07db585cd2e56ef07ca295199d45ff8a79aece GIT binary patch literal 25380 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 TpD&;PW1s&AEnOIh0FVIyP%dAe literal 0 HcmV?d00001 diff --git a/Ezmlm/tags/packages/libemail-ezmlm-perl_0.08.2-1_all.deb b/Ezmlm/tags/packages/libemail-ezmlm-perl_0.08.2-1_all.deb new file mode 100644 index 0000000000000000000000000000000000000000..b508ac85b13bfee59d79110ee27ef8c07bf86f80 GIT binary patch literal 37034 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 literal 0 HcmV?d00001