/[notice-sender]/trunk/Nos.pm
This is repository of my old source code which isn't updated any more. Go to git.rot13.org for current projects!
ViewVC logotype

Diff of /trunk/Nos.pm

Parent Directory Parent Directory | Revision Log Revision Log | View Patch Patch

revision 60 by dpavlin, Tue Jun 21 21:24:10 2005 UTC revision 81 by dpavlin, Fri Aug 26 06:13:44 2005 UTC
# Line 16  our @EXPORT_OK = ( @{ $EXPORT_TAGS{'all' Line 16  our @EXPORT_OK = ( @{ $EXPORT_TAGS{'all'
16  our @EXPORT = qw(  our @EXPORT = qw(
17  );  );
18    
19  our $VERSION = '0.5';  our $VERSION = '0.8';
20    
21  use Class::DBI::Loader;  use Class::DBI::Loader;
22  use Email::Valid;  use Email::Valid;
# Line 27  use Email::Simple; Line 27  use Email::Simple;
27  use Email::Address;  use Email::Address;
28  use Mail::DeliveryStatus::BounceParser;  use Mail::DeliveryStatus::BounceParser;
29  use Class::DBI::AbstractSearch;  use Class::DBI::AbstractSearch;
30    use SQL::Abstract;
31    use Mail::Alias;
32    use Cwd qw(abs_path);
33    
34    
35  =head1 NAME  =head1 NAME
# Line 60  encoded) or anything else. Line 63  encoded) or anything else.
63  It will just queue your e-mail message to particular list (sending it to  It will just queue your e-mail message to particular list (sending it to
64  possibly remote Notice Sender SOAP server just once), send it out at  possibly remote Notice Sender SOAP server just once), send it out at
65  reasonable rate (so that it doesn't flood your e-mail infrastructure) and  reasonable rate (so that it doesn't flood your e-mail infrastructure) and
66  track replies.  keep track replies.
67    
68  It is best used to send smaller number of messages to more-or-less fixed  It is best used to send small number of messages to more-or-less fixed
69  list of recipients while allowing individual responses to be examined.  list of recipients while allowing individual responses to be examined.
70  Tipical use include replacing php e-mail sending code with SOAP call to  Tipical use include replacing php e-mail sending code with SOAP call to
71  Notice Sender. It does support additional C<ext_id> field for each member  Notice Sender. It does support additional C<ext_id> field for each member
# Line 70  which can be used to track some unique i Line 73  which can be used to track some unique i
73  particular user.  particular user.
74    
75  It comes with command-line utility C<sender.pl> which can be used to perform  It comes with command-line utility C<sender.pl> which can be used to perform
76  all available operation from scripts (see C<perldoc sender.pl>).  all available operation from scripts (see C<sender.pl --man>).
77  This command is also useful for debugging while writing client SOAP  This command is also useful for debugging while writing client SOAP
78  application.  application.
79    
# Line 118  sub new { Line 121  sub new {
121  }  }
122    
123    
124  =head2 new_list  =head2 create_list
125    
126  Create new list. Required arguments are name of C<list> and  Create new list. Required arguments are name of C<list>, C<email> address
127  C<email> address.  and path to C<aliases> file.
128    
129   $nos->new_list(   $nos->create_list(
130          list => 'My list',          list => 'My list',
131          from => 'Outgoing from comment',          from => 'Outgoing from comment',
132          email => 'my-list@example.com',          email => 'my-list@example.com',
133            aliases => '/etc/mail/mylist',
134            archive => '/path/to/mbox/archive',
135   );   );
136    
137  Returns ID of newly created list.  Returns ID of newly created list.
# Line 135  Calls internally C<_add_list>, see detai Line 140  Calls internally C<_add_list>, see detai
140    
141  =cut  =cut
142    
143  sub new_list {  sub create_list {
144          my $self = shift;          my $self = shift;
145    
146          my $arg = {@_};          my $arg = {@_};
# Line 154  sub new_list { Line 159  sub new_list {
159  }  }
160    
161    
162    =head2 drop_list
163    
164    Delete list from database.
165    
166     my $ok = drop_list(
167            list => 'My list'
168            aliases => '/etc/mail/mylist',
169     );
170    
171    Returns false if list doesn't exist.
172    
173    =cut
174    
175    sub drop_list {
176            my $self = shift;
177    
178            my $args = {@_};
179    
180            croak "need list to delete" unless ($args->{'list'});
181    
182            $args->{'list'} = lc($args->{'list'});
183    
184            my $aliases = $args->{'aliases'} || croak "need path to aliases file";
185    
186            my $lists = $self->{'loader'}->find_class('lists');
187    
188            my $this_list = $lists->search( name => $args->{'list'} )->first || return;
189    
190            $self->_remove_alias( email => $this_list->email, aliases => $aliases);
191    
192            $this_list->delete || croak "can't delete list\n";
193    
194            return $lists->dbi_commit || croak "can't commit";
195    }
196    
197    
198  =head2 add_member_to_list  =head2 add_member_to_list
199    
200  Add new member to list  Add new member to list
# Line 227  List all members of some list. Line 268  List all members of some list.
268          list => 'My list',          list => 'My list',
269   );   );
270    
271  Returns array of hashes with user informations like this:  Returns array of hashes with user information like this:
272    
273   $member = {   $member = {
274          name => 'Dobrica Pavlinusic',          name => 'Dobrica Pavlinusic',
# Line 343  sub delete_member_from_list { Line 384  sub delete_member_from_list {
384          my $this_user = $user->search( email => $args->{'email'} )->first || croak "can't find user: ".$args->{'email'};          my $this_user = $user->search( email => $args->{'email'} )->first || croak "can't find user: ".$args->{'email'};
385          my $this_list = $list->search( name => $args->{'list'} )->first || croak "can't find list: ".$args->{'list'};          my $this_list = $list->search( name => $args->{'list'} )->first || croak "can't find list: ".$args->{'list'};
386    
387          my $this_user_list = $user_list->search_where( list_id => $this_list->id, user_id => $this_list->id )->first || return;          my $this_user_list = $user_list->search_where( list_id => $this_list->id, user_id => $this_user->id )->first || return;
388    
389          $this_user_list->delete || croak "can't delete user from list\n";          $this_user_list->delete || croak "can't delete user from list\n";
390    
# Line 435  Send e-mail using SMTP server at 127.0.0 Line 476  Send e-mail using SMTP server at 127.0.0
476    
477  =back  =back
478    
479    Any other driver name will try to use C<Email::Send::that_driver> module.
480    
481  Default sleep wait between two messages is 3 seconds.  Default sleep wait between two messages is 3 seconds.
482    
483    This method will return number of succesfully sent messages.
484    
485  =cut  =cut
486    
487  sub send_queued_messages {  sub send_queued_messages {
# Line 449  sub send_queued_messages { Line 494  sub send_queued_messages {
494          my $sleep = $arg->{'sleep'};          my $sleep = $arg->{'sleep'};
495          $sleep ||= 3 unless defined($sleep);          $sleep ||= 3 unless defined($sleep);
496    
497            # number of messages sent o.k.
498            my $ok = 0;
499    
500          my $email_send_driver = 'Email::Send::IO';          my $email_send_driver = 'Email::Send::IO';
501          my @email_send_options;          my @email_send_options;
502    
503          if (lc($driver) eq 'smtp') {          if (lc($driver) eq 'smtp') {
504                  $email_send_driver = 'Email::Send::SMTP';                  $email_send_driver = 'Email::Send::SMTP';
505                  @email_send_options = ['127.0.0.1'];                  @email_send_options = ['127.0.0.1'];
506            } elsif ($driver && $driver ne '') {
507                    $email_send_driver = 'Email::Send::' . $driver;
508          } else {          } else {
509                  warn "dumping all messages to STDERR\n";                  warn "dumping all messages to STDERR\n";
510          }          }
# Line 489  sub send_queued_messages { Line 539  sub send_queued_messages {
539                          if ($sent->search( message_id => $m->message_id, user_id => $u->user_id )) {                          if ($sent->search( message_id => $m->message_id, user_id => $u->user_id )) {
540                                  print "SKIP $to_email message allready sent\n";                                  print "SKIP $to_email message allready sent\n";
541                          } else {                          } else {
542                                  print "=> $to_email\n";                                  print "=> $to_email ";
543    
544                                  my $secret = $m->list_id->name . " " . $u->user_id->email . " " . $m->message_id;                                  my $secret = $m->list_id->name . " " . $u->user_id->email . " " . $m->message_id;
545                                  my $auth = Email::Auth::AddressHash->new( $secret, $self->{'hash_len'} );                                  my $auth = Email::Auth::AddressHash->new( $secret, $self->{'hash_len'} );
# Line 515  sub send_queued_messages { Line 565  sub send_queued_messages {
565                                  $m_obj->header_set('X-Nos-Hash', $hash);                                  $m_obj->header_set('X-Nos-Hash', $hash);
566    
567                                  # really send e-mail                                  # really send e-mail
568                                    my $sent_status;
569    
570                                  if (@email_send_options) {                                  if (@email_send_options) {
571                                          send $email_send_driver => $m_obj->as_string, @email_send_options;                                          $sent_status = send $email_send_driver => $m_obj->as_string, @email_send_options;
572                                  } else {                                  } else {
573                                          send $email_send_driver => $m_obj->as_string;                                          $sent_status = send $email_send_driver => $m_obj->as_string;
574                                  }                                  }
575    
576                                  $sent->create({                                  croak "can't send e-mail: $sent_status\n\nOriginal e-mail follows:\n".$m_obj->as_string unless ($sent_status);
577                                          message_id => $m->message_id,                                  my @bad;
578                                          user_id => $u->user_id,                                  @bad = @{ $sent_status->prop('bad') } if (eval { $sent_status->can('prop') });
579                                          hash => $hash,                                  croak "failed sending to ",join(",",@bad) if (@bad);
580                                  });  
581                                  $sent->dbi_commit;                                  if ($sent_status) {
582    
583                                            $sent->create({
584                                                    message_id => $m->message_id,
585                                                    user_id => $u->user_id,
586                                                    hash => $hash,
587                                            });
588                                            $sent->dbi_commit;
589    
590                                            print " - $sent_status\n";
591    
592                                            $ok++;
593                                    } else {
594                                            warn "ERROR: $sent_status\n";
595                                    }
596    
597                                  if ($sleep) {                                  if ($sleep) {
598                                          warn "sleeping $sleep seconds\n";                                          warn "sleeping $sleep seconds\n";
# Line 539  sub send_queued_messages { Line 605  sub send_queued_messages {
605                  $m->dbi_commit;                  $m->dbi_commit;
606          }          }
607    
608            return $ok;
609    
610  }  }
611    
612  =head2 inbox_message  =head2 inbox_message
# Line 637  sub inbox_message { Line 705  sub inbox_message {
705  #       print "message_id: ",($message_id || "not found")," -- $is_bounce\n";  #       print "message_id: ",($message_id || "not found")," -- $is_bounce\n";
706  }  }
707    
708    =head2 received_messages
709    
710    Returns all received messages for given list or user.
711    
712     my @received = $nos->received_messages(
713            list => 'My list',
714            email => "john.doe@example.com",
715            from_date => '2005-01-01 10:15:00',
716            to_date => '2005-01-01 12:00:00',
717            message => 0,
718     );
719    
720    If don't specify C<list> or C<email> it will return all received messages.
721    Results will be sorted by received date, oldest first.
722    
723    Other optional parametars include:
724    
725    =over 10
726    
727    =item from_date
728    
729    Date (in ISO format) for lower limit of dates received
730    
731    =item to_date
732    
733    Return just messages older than this date
734    
735    =item message
736    
737    Include whole received message in result. This will probably make result
738    array very large. Use with care.
739    
740    =back
741    
742    Date ranges are inclusive, so results will include messages sent on
743    particular date specified with C<date_from> or C<date_to>.
744    
745    Each element in returned array will have following structure:
746    
747     my $row = {
748            id => 42,                       # unique ID of received message
749            list => 'My list',              # useful if filtering by email
750            ext_id => 9999,                 # ext_id from message sender
751            email => 'jdoe@example.com',    # e-mail of message sender
752            bounced => 0,                   # true if message is bounce
753            date => '2005-08-24 18:57:24',  # date of receival in ISO format
754     }
755    
756    If you specified C<message> option, this hash will also have C<message> key
757    which will contain whole received message.
758    
759    =cut
760    
761    sub received_messages {
762            my $self = shift;
763    
764            my $arg = {@_} if (@_);
765    
766    #       croak "need list name or email" unless ($arg->{'list'} || $arg->{'email'});
767    
768            my $sql = qq{
769                            select
770                                    received.id as id,
771                                    lists.name as list,
772                                    users.ext_id as ext_id,
773                                    users.email as email,
774            };
775            $sql .= qq{             message,} if ($arg->{'message'});
776            $sql .= qq{
777                                    bounced,received.date as date
778                            from received
779                            join lists on lists.id = list_id
780                            join users on users.id = user_id
781            };
782    
783            my $order = qq{ order by date asc };
784    
785            my $where;
786    
787            $where->{'lists.name'} = lc($arg->{'list'}) if ($arg->{'list'});
788            $where->{'users.email'} = lc($arg->{'email'}) if ($arg->{'email'});
789            $where->{'received.date'} = { '>=', $arg->{'date_from'} } if ($arg->{'date_from'});
790            $where->{'received.date'} = { '<=', $arg->{'date_to'} } if ($arg->{'date_to'});
791    
792            # hum, yammy one-liner
793            my($stmt, @bind)  = SQL::Abstract->new->where($where);
794    
795            my $dbh = $self->{'loader'}->find_class('received')->db_Main;
796    
797            my $sth = $dbh->prepare($sql . $stmt . $order);
798            $sth->execute(@bind);
799            return $sth->fetchall_hash;
800    }
801    
802    
803  =head1 INTERNAL METHODS  =head1 INTERNAL METHODS
804    
805  Beware of dragons! You shouldn't need to call those methods directly.  Beware of dragons! You shouldn't need to call those methods directly.
806    
807    
808    =head2 _add_aliases
809    
810    Add or update alias in C</etc/aliases> (or equivalent) file for selected list
811    
812     my $ok = $nos->add_aliases(
813            list => 'My list',
814            email => 'my-list@example.com',
815            aliases => '/etc/mail/mylist',
816            archive => '/path/to/mbox/archive',
817    
818     );
819    
820    C<archive> parametar is optional.
821    
822    Return false on failure.
823    
824    =cut
825    
826    sub _add_aliases {
827            my $self = shift;
828    
829            my $arg = {@_};
830    
831            foreach my $o (qw/list email aliases/) {
832                    croak "need $o option" unless ($arg->{$o});
833            }
834    
835            my $aliases = $arg->{'aliases'};
836            my $email = $arg->{'email'};
837            my $list = $arg->{'list'};
838    
839            unless (-e $aliases) {
840                    warn "aliases file $aliases doesn't exist, creating empty\n";
841                    open(my $fh, '>', $aliases) || croak "can't create $aliases: $!";
842                    close($fh);
843                    chmod 0777, $aliases || warn "can't change permission to 0777";
844            }
845    
846            die "FATAL: aliases file $aliases is not writable\n" unless (-w $aliases);
847    
848            my $a = new Mail::Alias($aliases) || croak "can't open aliases file $aliases: $!";
849    
850            my $target = '';
851    
852            if (my $archive = $arg->{'archive'}) {
853                    $target .= "$archive, ";
854    
855                    if (! -e $archive) {
856                            warn "please make sure that file $archive is writable for your e-mail user (defaulting to bad 777 permission for now)";
857    
858                            open(my $fh, '>', $archive) || croak "can't create archive file $archive: $!";
859                            close($fh);
860                            chmod 0777, $archive || croak "can't chmod archive file $archive to 0777: $!";
861                    }
862            }
863    
864            # resolve my path to absolute one
865            my $self_path = abs_path($0);
866            $self_path =~ s#/[^/]+$##;
867            $self_path =~ s#/t/*$#/#;
868    
869            $target .= qq#| cd $self_path && ./sender.pl --inbox="$list"#;
870    
871            if ($a->exists($email)) {
872                    $a->update($email, $target) or croak "can't update alias ".$a->error_check;
873            } else {
874                    $a->append($email, $target) or croak "can't add alias ".$a->error_check;
875            }
876    
877            #$a->write($aliases) or croak "can't save aliases $aliases ".$a->error_check;
878    
879            return 1;
880    }
881    
882  =head2 _add_list  =head2 _add_list
883    
884  Create new list  Create new list
# Line 650  Create new list Line 887  Create new list
887          list => 'My list',          list => 'My list',
888          from => 'Outgoing from comment',          from => 'Outgoing from comment',
889          email => 'my-list@example.com',          email => 'my-list@example.com',
890            aliases => '/etc/mail/mylist',
891   );   );
892    
893  Returns C<Class::DBI> object for created list.  Returns C<Class::DBI> object for created list.
# Line 668  sub _add_list { Line 906  sub _add_list {
906    
907          my $name = lc($arg->{'list'}) || confess "can't add list without name";          my $name = lc($arg->{'list'}) || confess "can't add list without name";
908          my $email = lc($arg->{'email'}) || confess "can't add list without e-mail";          my $email = lc($arg->{'email'}) || confess "can't add list without e-mail";
909            my $aliases = lc($arg->{'aliases'}) || confess "can't add list without path to aliases file";
910    
911          my $from_addr = $arg->{'from'};          my $from_addr = $arg->{'from'};
912    
913          my $lists = $self->{'loader'}->find_class('lists');          my $lists = $self->{'loader'}->find_class('lists');
914    
915            $self->_add_aliases(
916                    list => $name,
917                    email => $email,
918                    aliases => $aliases,
919            ) || warn "can't add alias $email for list $name";
920    
921          my $l = $lists->find_or_create({          my $l = $lists->find_or_create({
922                  name => $name,                  name => $name,
923                  email => $email,                  email => $email,
# Line 691  sub _add_list { Line 937  sub _add_list {
937  }  }
938    
939    
940    
941  =head2 _get_list  =head2 _get_list
942    
943  Get list C<Class::DBI> object.  Get list C<Class::DBI> object.
# Line 711  sub _get_list { Line 958  sub _get_list {
958          return $lists->search({ name => lc($name) })->first;          return $lists->search({ name => lc($name) })->first;
959  }  }
960    
961    
962    =head2 _remove_alias
963    
964    Remove list alias
965    
966     my $ok = $nos->_remove_alias(
967            email => 'mylist@example.com',
968            aliases => '/etc/mail/mylist',
969     );
970    
971    Returns true if list is removed or false if list doesn't exist. Dies in case of error.
972    
973    =cut
974    
975    sub _remove_alias {
976            my $self = shift;
977    
978            my $arg = {@_};
979    
980            my $email = lc($arg->{'email'}) || confess "can't remove alias without email";
981            my $aliases = lc($arg->{'aliases'}) || confess "can't remove alias without list";
982    
983            my $a = new Mail::Alias($aliases) || croak "can't open aliases file $aliases: $!";
984    
985            if ($a->exists($email)) {
986                    $a->delete($email) || croak "can't remove alias $email";
987            } else {
988                    return 0;
989            }
990    
991            return 1;
992    
993    }
994    
995  ###  ###
996  ### SOAP  ### SOAP
997  ###  ###
# Line 735  methods below). Line 1016  methods below).
1016    
1017  my $nos;  my $nos;
1018    
1019    
1020    =head2 new
1021    
1022    Create new SOAP object
1023    
1024     my $soap = new Nos::SOAP(
1025            dsn => 'dbi:Pg:dbname=notices',
1026            user => 'dpavlin',
1027            passwd => '',
1028            debug => 1,
1029            verbose => 1,
1030            hash_len => 8,
1031            aliases => '/etc/aliases',
1032     );
1033    
1034    If you are writing SOAP server (like C<soap.cgi> example), you will need to
1035    call this method once to make new instance of Nos::SOAP and specify C<dsn>
1036    and options for it.
1037    
1038    =cut
1039    
1040  sub new {  sub new {
1041          my $class = shift;          my $class = shift;
1042          my $self = {@_};          my $self = {@_};
1043    
1044            croak "need aliases parametar" unless ($self->{'aliases'});
1045    
1046          bless($self, $class);          bless($self, $class);
1047    
1048          $nos = new Nos( @_ ) || die "can't create Nos object";          $nos = new Nos( @_ ) || die "can't create Nos object";
# Line 746  sub new { Line 1051  sub new {
1051  }  }
1052    
1053    
1054  =head2 NewList  =head2 CreateList
1055    
1056   $message_id = NewList(   $message_id = CreateList(
1057          list => 'My list',          list => 'My list',
1058          from => 'Name of my list',          from => 'Name of my list',
1059          email => 'my-list@example.com'          email => 'my-list@example.com'
# Line 756  sub new { Line 1061  sub new {
1061    
1062  =cut  =cut
1063    
1064  sub NewList {  sub CreateList {
1065          my $self = shift;          my $self = shift;
1066    
1067            my $aliases = $nos->{'aliases'} || croak "need 'aliases' argument to new constructor";
1068    
1069          if ($_[0] !~ m/^HASH/) {          if ($_[0] !~ m/^HASH/) {
1070                  return $nos->new_list(                  return $nos->create_list(
1071                          list => $_[0], from => $_[1], email => $_[2],                          list => $_[0], from => $_[1], email => $_[2],
1072                            aliases => $aliases,
1073                  );                  );
1074          } else {          } else {
1075                  return $nos->new_list( %{ shift @_ } );                  return $nos->create_list( %{ shift @_ }, aliases => $aliases );
1076          }          }
1077  }  }
1078    
1079    
1080    =head2 DropList
1081    
1082     $ok = DropList(
1083            list => 'My list',
1084     );
1085    
1086    =cut
1087    
1088    sub DropList {
1089            my $self = shift;
1090    
1091            my $aliases = $nos->{'aliases'} || croak "need 'aliases' argument to new constructor";
1092    
1093            if ($_[0] !~ m/^HASH/) {
1094                    return $nos->drop_list(
1095                            list => $_[0],
1096                            aliases => $aliases,
1097                    );
1098            } else {
1099                    return $nos->drop_list( %{ shift @_ }, aliases => $aliases );
1100            }
1101    }
1102    
1103  =head2 AddMemberToList  =head2 AddMemberToList
1104    
1105   $member_id = AddMemberToList(   $member_id = AddMemberToList(
# Line 814  sub ListMembers { Line 1145  sub ListMembers {
1145                  $list_name = $_[0]->{'list'};                  $list_name = $_[0]->{'list'};
1146          }          }
1147    
1148          return $nos->list_members( list => $list_name );          return [ $nos->list_members( list => $list_name ) ];
1149    }
1150    
1151    
1152    =head2 DeleteMemberFromList
1153    
1154     $member_id = DeleteMemberFromList(
1155            list => 'My list',
1156            email => 'e-mail@example.com',
1157     );
1158    
1159    =cut
1160    
1161    sub DeleteMemberFromList {
1162            my $self = shift;
1163    
1164            if ($_[0] !~ m/^HASH/) {
1165                    return $nos->delete_member_from_list(
1166                            list => $_[0], email => $_[1],
1167                    );
1168            } else {
1169                    return $nos->delete_member_from_list( %{ shift @_ } );
1170            }
1171  }  }
1172    
1173    
1174  =head2 AddMessageToList  =head2 AddMessageToList
1175    
1176   $message_id = AddMessageToList(   $message_id = AddMessageToList(
# Line 838  sub AddMessageToList { Line 1192  sub AddMessageToList {
1192          }          }
1193  }  }
1194    
1195    =head2 MessagesReceived
1196    
1197    Return statistics about received messages.
1198    
1199     my @result = MessagesReceived(
1200            list => 'My list',
1201            email => 'jdoe@example.com',
1202            from_date => '2005-01-01 10:15:00',
1203            to_date => '2005-01-01 12:00:00',
1204            message => 0,
1205     );
1206    
1207    You must specify C<list> or C<email> or any combination of those two. Other
1208    parametars are optional.
1209    
1210    For format of returned array element see C<received_messages>.
1211    
1212    =cut
1213    
1214    sub MessagesReceived {
1215            my $self = shift;
1216    
1217            if ($_[0] !~ m/^HASH/) {
1218                    die "need at least list or email" unless (scalar @_ < 2);
1219                    return $nos->received_messages(
1220                            list => $_[0], email => $_[1],
1221                            from_date => $_[2], to_date => $_[3],
1222                            message => $_[4]
1223                    );
1224            } else {
1225                    my $arg = shift;
1226                    die "need list or email argument" unless ($arg->{'list'} || $arg->{'email'});
1227                    return $nos->received_messages( $arg );
1228            }
1229    }
1230    
1231  ###  ###
1232    
1233    =head1 NOTE ON ARRAYS IN SOAP
1234    
1235    Returning arrays from SOAP calls is somewhat fuzzy (at least to me). It
1236    seems that SOAP::Lite client thinks that it has array with one element which
1237    is array of hashes with data.
1238    
1239  =head1 EXPORT  =head1 EXPORT
1240    
1241  Nothing.  Nothing.

Legend:
Removed from v.60  
changed lines
  Added in v.81

  ViewVC Help
Powered by ViewVC 1.1.26