package Nos; use 5.008; use strict; use warnings; require Exporter; our @ISA = qw(Exporter); our %EXPORT_TAGS = ( 'all' => [ qw( ) ] ); our @EXPORT_OK = ( @{ $EXPORT_TAGS{'all'} } ); our @EXPORT = qw( ); our $VERSION = '0.9'; use Class::DBI::Loader; use Email::Valid; use Email::Send; use Carp; use Email::Auth::AddressHash; use Email::Simple; use Email::Address; use Mail::DeliveryStatus::BounceParser; use Class::DBI::AbstractSearch; use SQL::Abstract; use Mail::Alias; use Cwd qw(abs_path); =head1 NAME Nos - Notice Sender core module =head1 SYNOPSIS use Nos; my $nos = new Nos(); =head1 DESCRIPTION Notice sender is mail handler. It is not MTA, since it doesn't know how to receive e-mails or send them directly to other hosts. It is not mail list manager because it requires programming to add list members and send messages. You can think of it as mechanisam for off-loading your e-mail sending to remote server using SOAP service. It's concept is based around B. Each list can have zero or more B. Each list can have zero or more B. Here comes a twist: each outgoing message will have unique e-mail generated, so Notice Sender will be able to link received replies (or bounces) with outgoing messages. It doesn't do much more than that. It B create MIME encoded e-mail, send attachments, handle 8-bit characters in headers (which have to be encoded) or anything else. It will just queue your e-mail message to particular list (sending it to possibly remote Notice Sender SOAP server just once), send it out at reasonable rate (so that it doesn't flood your e-mail infrastructure) and keep track replies. It is best used to send small number of messages to more-or-less fixed list of recipients while allowing individual responses to be examined. Tipical use include replacing php e-mail sending code with SOAP call to Notice Sender. It does support additional C field for each member which can be used to track some unique identifier from remote system for particular user. It comes with command-line utility C which can be used to perform all available operation from scripts (see C). This command is also useful for debugging while writing client SOAP application. =head1 METHODS =head2 new Create new instance specifing database, user, password and options. my $nos = new Nos( dsn => 'dbi:Pg:dbname=notices', user => 'dpavlin', passwd => '', debug => 1, verbose => 1, hash_len => 8, full_hostname_in_aliases => 0, ); Parametar C defines length of hash which will be added to each outgoing e-mail message to ensure that replies can be linked with sent e-mails. C will turn on old behaviour (not supported by Postfix postalias) to include full hostname in aliases file. =cut sub new { my $class = shift; my $self = {@_}; bless($self, $class); croak "need at least dsn" unless ($self->{'dsn'}); $self->{'loader'} = Class::DBI::Loader->new( debug => $self->{'debug'}, dsn => $self->{'dsn'}, user => $self->{'user'}, password => $self->{'passwd'}, namespace => "Nos", additional_classes => qw/Class::DBI::AbstractSearch/, # additional_base_classes => qw/My::Stuff/, relationships => 1, ) || croak "can't init Class::DBI::Loader"; $self->{'hash_len'} ||= 8; $self ? return $self : return undef; } =head2 create_list Create new list. Required arguments are name of C, C address and path to C file. $nos->create_list( list => 'My list', from => 'Outgoing from comment', email => 'my-list@example.com', aliases => '/etc/mail/mylist', archive => '/path/to/mbox/archive', ); Returns ID of newly created list. Calls internally C<_add_list>, see details there. =cut sub create_list { my $self = shift; my $arg = {@_}; confess "need list name" unless ($arg->{'list'}); confess "need list email" unless ($arg->{'email'}); $arg->{'list'} = lc($arg->{'list'}); $arg->{'email'} = lc($arg->{'email'}); my $l = $self->_get_list($arg->{'list'}) || $self->_add_list( @_ ) || return undef; return $l->id; } =head2 drop_list Delete list from database. my $ok = drop_list( list => 'My list' aliases => '/etc/mail/mylist', ); Returns false if list doesn't exist. =cut sub drop_list { my $self = shift; my $args = {@_}; croak "need list to delete" unless ($args->{'list'}); $args->{'list'} = lc($args->{'list'}); my $aliases = $args->{'aliases'} || croak "need path to aliases file"; my $lists = $self->{'loader'}->find_class('lists'); my $this_list = $lists->search( name => $args->{'list'} )->first || return; $self->_remove_alias( email => $this_list->email, aliases => $aliases); $this_list->delete || croak "can't delete list\n"; return $lists->dbi_commit || croak "can't commit"; } =head2 add_member_to_list Add new member to list $nos->add_member_to_list( list => "My list", email => "john.doe@example.com", name => "John A. Doe", ext_id => 42, ); C and C parametars are optional. Return member ID if user is added. =cut sub add_member_to_list { my $self = shift; my $arg = {@_}; my $email = lc($arg->{'email'}) || croak "can't add user without e-mail"; my $name = $arg->{'name'} || ''; my $list_name = lc($arg->{'list'}) || croak "need list name"; my $ext_id = $arg->{'ext_id'}; my $list = $self->_get_list($list_name) || croak "list $list_name doesn't exist"; if (! Email::Valid->address($email)) { carp "SKIPPING $name <$email>\n"; return 0; } carp "# $name <$email>\n" if ($self->{'verbose'}); my $users = $self->{'loader'}->find_class('users'); my $user_list = $self->{'loader'}->find_class('user_list'); my $this_user = $users->find_or_create({ email => $email, }) || croak "can't find or create member\n"; if ($name && $this_user->name ne $name) { $this_user->name($name || ''); $this_user->update; } if (defined($ext_id) && ($this_user->ext_id || '') ne $ext_id) { $this_user->ext_id($ext_id); $this_user->update; } my $user_on_list = $user_list->find_or_create({ user_id => $this_user->id, list_id => $list->id, }) || croak "can't add user to list"; $list->dbi_commit; $this_user->dbi_commit; $user_on_list->dbi_commit; return $this_user->id; } =head2 list_members List all members of some list. my @members = list_members( list => 'My list', ); Returns array of hashes with user information like this: $member = { name => 'Dobrica Pavlinusic', email => 'dpavlin@rot13.org } If list is not found, returns false. If there is C in user data, it will also be returned. =cut sub list_members { my $self = shift; my $args = {@_}; my $list_name = lc($args->{'list'}) || confess "need list name"; my $lists = $self->{'loader'}->find_class('lists'); my $user_list = $self->{'loader'}->find_class('user_list'); my $this_list = $lists->search( name => $list_name )->first || return; my @results; foreach my $user_on_list ($user_list->search(list_id => $this_list->id)) { my $row = { name => $user_on_list->user_id->name, email => $user_on_list->user_id->email, }; my $ext_id = $user_on_list->user_id->ext_id; $row->{'ext_id'} = $ext_id if (defined($ext_id)); push @results, $row; } return @results; } =head2 delete_member Delete member from database. my $ok = delete_member( name => 'Dobrica Pavlinusic' ); my $ok = delete_member( email => 'dpavlin@rot13.org' ); Returns false if user doesn't exist. This function will delete member from all lists (by cascading delete), so it shouldn't be used lightly. =cut sub delete_member { my $self = shift; my $args = {@_}; croak "need name or email of user to delete" unless ($args->{'name'} || $args->{'email'}); $args->{'email'} = lc($args->{'email'}) if ($args->{'email'}); my $key = 'name'; $key = 'email' if ($args->{'email'}); my $users = $self->{'loader'}->find_class('users'); my $this_user = $users->search( $key => $args->{$key} )->first || return; $this_user->delete || croak "can't delete user\n"; return $users->dbi_commit || croak "can't commit"; } =head2 delete_member_from_list Delete member from particular list. my $ok = delete_member_from_list( list => 'My list', email => 'dpavlin@rot13.org', ); Returns false if user doesn't exist on that particular list. It will die if list or user doesn't exist. You have been warned (you might want to eval this functon to prevent it from croaking). =cut sub delete_member_from_list { my $self = shift; my $args = {@_}; croak "need list name and email of user to delete" unless ($args->{'list'} && $args->{'email'}); $args->{'list'} = lc($args->{'list'}); $args->{'email'} = lc($args->{'email'}); my $user = $self->{'loader'}->find_class('users'); my $list = $self->{'loader'}->find_class('lists'); my $user_list = $self->{'loader'}->find_class('user_list'); my $this_user = $user->search( email => $args->{'email'} )->first || croak "can't find user: ".$args->{'email'}; my $this_list = $list->search( name => $args->{'list'} )->first || croak "can't find list: ".$args->{'list'}; my $this_user_list = $user_list->search_where( list_id => $this_list->id, user_id => $this_user->id )->first || return; $this_user_list->delete || croak "can't delete user from list\n"; return $user_list->dbi_commit || croak "can't commit"; } =head2 add_message_to_list Adds message to one list's queue for later sending. $nos->add_message_to_list( list => 'My list', message => 'Subject: welcome to list This is example message ', ); On success returns ID of newly created (or existing) message. Only required header in e-mail is C. C and C headers will be automatically generated, but if you want to use own headers, just include them in messages. =cut sub add_message_to_list { my $self = shift; my $args = {@_}; my $list_name = lc($args->{'list'}) || confess "need list name"; my $message_text = $args->{'message'} || croak "need message"; my $m = Email::Simple->new($message_text) || croak "can't parse message"; warn "message doesn't have Subject header\n" unless( $m->header('Subject') ); my $lists = $self->{'loader'}->find_class('lists'); my $this_list = $lists->search( name => $list_name, )->first || croak "can't find list $list_name"; my $messages = $self->{'loader'}->find_class('messages'); my $this_message = $messages->find_or_create({ message => $message_text }) || croak "can't insert message"; $this_message->dbi_commit() || croak "can't add message"; my $queue = $self->{'loader'}->find_class('queue'); $queue->find_or_create({ message_id => $this_message->id, list_id => $this_list->id, }) || croak "can't add message ",$this_message->id," to list ",$this_list->id, ": ",$this_list->name; $queue->dbi_commit || croak "can't add message to list ",$this_list->name; return $this_message->id; } =head2 send_queued_messages Send queued messages or just ones for selected list $nos->send_queued_messages( list => 'My list', driver => 'smtp', sleep => 3, verbose => 1, ); Second option is driver which will be used for e-mail delivery. If not specified, C driver will be used which will dump e-mail to C. Other valid drivers are: =over 10 =item smtp Send e-mail using SMTP server at 127.0.0.1 =item verbose Display diagnostic output to C and C. =back Any other driver name will try to use C module. Default sleep wait between two messages is 3 seconds. This method will return number of succesfully sent messages. =cut sub send_queued_messages { my $self = shift; my $arg = {@_}; my $list_name = lc($arg->{'list'}) || ''; my $driver = $arg->{'driver'} || ''; my $sleep = $arg->{'sleep'}; my $verbose = $arg->{verbose}; $sleep ||= 3 unless defined($sleep); # number of messages sent o.k. my $ok = 0; my $email_send_driver = 'Email::Send::IO'; my @email_send_options; if (lc($driver) eq 'smtp') { $email_send_driver = 'Email::Send::SMTP'; @email_send_options = ['127.0.0.1']; } elsif ($driver && $driver ne '') { $email_send_driver = 'Email::Send::' . $driver; } else { warn "dumping all messages to STDERR\n" if ($verbose); } my $lists = $self->{'loader'}->find_class('lists'); my $queue = $self->{'loader'}->find_class('queue'); my $user_list = $self->{'loader'}->find_class('user_list'); my $sent = $self->{'loader'}->find_class('sent'); my $my_q; if ($list_name ne '') { my $l_id = $lists->search_like( name => $list_name )->first || croak "can't find list $list_name"; $my_q = $queue->search_like( list_id => $l_id ) || croak "can't find list $list_name"; } else { $my_q = $queue->retrieve_all; } while (my $m = $my_q->next) { next if ($m->all_sent); print "sending message ",$m->message_id," enqueued on ",$m->date," to list ",$m->list_id->name,"\n" if ($verbose); my $msg = $m->message_id->message; foreach my $u ($user_list->search(list_id => $m->list_id)) { my $to_email = $u->user_id->email; my ($from,$domain) = split(/@/, $u->list_id->email, 2); if ($sent->search( message_id => $m->message_id, user_id => $u->user_id )) { print "SKIP $to_email message allready sent\n" if ($verbose); } else { print "=> $to_email " if ($verbose); my $secret = $m->list_id->name . " " . $u->user_id->email . " " . $m->message_id; my $auth = Email::Auth::AddressHash->new( $secret, $self->{'hash_len'} ); my $hash = $auth->generate_hash( $to_email ); my $from_addr; my $from_email_only = $from . "+" . $hash . ( $domain ? '@' . $domain : ''); $from_addr .= '"' . $u->list_id->from_addr . '" ' if ($u->list_id->from_addr); $from_addr .= '<' . $from_email_only . '>'; my $to = '"' . $u->user_id->name . '" <' . $to_email . '>'; my $m_obj = Email::Simple->new($msg) || croak "can't parse message"; $m_obj->header_set('Return-Path', $from_email_only) || croak "can't set Return-Path: header"; #$m_obj->header_set('Sender', $from_email_only) || croak "can't set Sender: header"; $m_obj->header_set('Errors-To', $from_email_only) || croak "can't set Errors-To: header"; $m_obj->header_set('From', $from_addr) || croak "can't set From: header"; $m_obj->header_set('To', $to) || croak "can't set To: header"; $m_obj->header_set('X-Nos-Version', $VERSION); $m_obj->header_set('X-Nos-Hash', $hash); # really send e-mail my $sent_status; if (@email_send_options) { $sent_status = send $email_send_driver => $m_obj->as_string, @email_send_options; } else { $sent_status = send $email_send_driver => $m_obj->as_string; } croak "can't send e-mail: $sent_status\n\nOriginal e-mail follows:\n".$m_obj->as_string unless ($sent_status); my @bad; @bad = @{ $sent_status->prop('bad') } if (eval { $sent_status->can('prop') }); croak "failed sending to ",join(",",@bad) if (@bad); if ($sent_status) { $sent->create({ message_id => $m->message_id, user_id => $u->user_id, hash => $hash, }); $sent->dbi_commit; print " - $sent_status\n" if ($verbose); $ok++; } else { warn "ERROR: $sent_status\n" if ($verbose); } if ($sleep) { warn "sleeping $sleep seconds\n" if ($verbose); sleep($sleep); } } } $m->all_sent(1); $m->update; $m->dbi_commit; } return $ok; } =head2 inbox_message Receive single message for list's inbox. my $ok = $nos->inbox_message( list => 'My list', message => $message, ); This method is used by C when receiving e-mail messages. =cut sub inbox_message { my $self = shift; my $arg = {@_}; return unless ($arg->{'message'}); croak "need list name" unless ($arg->{'list'}); $arg->{'list'} = lc($arg->{'list'}); my $this_list = $self->_get_list($arg->{'list'}) || croak "can't find list ".$arg->{'list'}."\n"; my $m = Email::Simple->new($arg->{'message'}) || croak "can't parse message"; my $to = $m->header('To') || die "can't find To: address in incomming message\n"; my $return_path = $m->header('Return-Path') || ''; my @addrs = Email::Address->parse( $to ); die "can't parse To: $to address\n" unless (@addrs); my $hl = $self->{'hash_len'} || confess "no hash_len?"; my $hash; foreach my $a (@addrs) { if ($a->address =~ m/\+([a-f0-9]{$hl})@/i) { $hash = $1; last; } } #warn "can't find hash in e-mail $to\n" unless ($hash); my $sent = $self->{'loader'}->find_class('sent'); # will use null if no matching message_id is found my $sent_msg; $sent_msg = $sent->search( hash => $hash )->first if ($hash); my ($message_id, $user_id) = (undef, undef); # init with NULL if ($sent_msg) { $message_id = $sent_msg->message_id || carp "no message_id"; $user_id = $sent_msg->user_id || carp "no user_id"; } else { #warn "can't find sender with hash $hash\n"; my $users = $self->{'loader'}->find_class('users'); my $from = $m->header('From'); $from = $1 if ($from =~ m/<(.*)>/); my $this_user = $users->search( email => lc($from) )->first; $user_id = $this_user->id if ($this_user); } my $is_bounce = 0; if ($return_path eq '<>' || $return_path eq '') { no warnings; my $bounce = eval { Mail::DeliveryStatus::BounceParser->new( $arg->{'message'}, { report_non_bounces=>1 }, ) }; #warn "can't check if this message is bounce!" if ($@); $is_bounce++ if ($bounce && $bounce->is_bounce); } my $received = $self->{'loader'}->find_class('received'); my $this_received = $received->find_or_create({ user_id => $user_id, list_id => $this_list->id, message_id => $message_id, message => $arg->{'message'}, bounced => $is_bounce, }) || croak "can't insert received message"; $this_received->dbi_commit; # print "message_id: ",($message_id || "not found")," -- $is_bounce\n"; } =head2 received_messages Returns all received messages for given list or user. my @received = $nos->received_messages( list => 'My list', email => "john.doe@example.com", from_date => '2005-01-01 10:15:00', to_date => '2005-01-01 12:00:00', message => 0, ); If don't specify C or C it will return all received messages. Results will be sorted by received date, oldest first. Other optional parametars include: =over 10 =item from_date Date (in ISO format) for lower limit of dates received =item to_date Return just messages older than this date =item message Include whole received message in result. This will probably make result array very large. Use with care. =back Date ranges are inclusive, so results will include messages sent on particular date specified with C or C. Each element in returned array will have following structure: my $row = { id => 42, # unique ID of received message list => 'My list', # useful if filtering by email ext_id => 9999, # ext_id from message sender email => 'jdoe@example.com', # e-mail of message sender bounced => 0, # true if message is bounce date => '2005-08-24 18:57:24', # date of receival in ISO format } If you specified C option, this hash will also have C key which will contain whole received message. =cut sub received_messages { my $self = shift; my $arg = {@_} if (@_); # croak "need list name or email" unless ($arg->{'list'} || $arg->{'email'}); my $sql = qq{ select received.id as id, lists.name as list, users.ext_id as ext_id, users.email as email, }; $sql .= qq{ message,} if ($arg->{'message'}); $sql .= qq{ bounced,received.date as date from received join lists on lists.id = list_id join users on users.id = user_id }; my $order = qq{ order by date asc }; my $where; $where->{'lists.name'} = lc($arg->{'list'}) if ($arg->{'list'}); $where->{'users.email'} = lc($arg->{'email'}) if ($arg->{'email'}); $where->{'received.date'} = { '>=', $arg->{'date_from'} } if ($arg->{'date_from'}); $where->{'received.date'} = { '<=', $arg->{'date_to'} } if ($arg->{'date_to'}); # hum, yammy one-liner my($stmt, @bind) = SQL::Abstract->new->where($where); my $dbh = $self->{'loader'}->find_class('received')->db_Main; my $sth = $dbh->prepare($sql . $stmt . $order); $sth->execute(@bind); return $sth->fetchall_hash; } =head1 INTERNAL METHODS Beware of dragons! You shouldn't need to call those methods directly. =head2 _add_aliases Add or update alias in C (or equivalent) file for selected list my $ok = $nos->add_aliases( list => 'My list', email => 'my-list@example.com', aliases => '/etc/mail/mylist', archive => '/path/to/mbox/archive', ); C parametar is optional. Return false on failure. =cut sub _add_aliases { my $self = shift; my $arg = {@_}; foreach my $o (qw/list email aliases/) { croak "need $o option" unless ($arg->{$o}); } my $aliases = $arg->{'aliases'}; my $email = $arg->{'email'}; my $list = $arg->{'list'}; unless (-e $aliases) { warn "aliases file $aliases doesn't exist, creating empty\n"; open(my $fh, '>', $aliases) || croak "can't create $aliases: $!"; close($fh); chmod 0777, $aliases || warn "can't change permission to 0777"; } die "FATAL: aliases file $aliases is not writable\n" unless (-w $aliases); my $a = new Mail::Alias($aliases) || croak "can't open aliases file $aliases: $!"; my $target = ''; if (my $archive = $arg->{'archive'}) { $target .= "$archive, "; if (! -e $archive) { warn "please make sure that file $archive is writable for your e-mail user (defaulting to bad 777 permission for now)"; open(my $fh, '>', $archive) || croak "can't create archive file $archive: $!"; close($fh); chmod 0777, $archive || croak "can't chmod archive file $archive to 0777: $!"; } } # resolve my path to absolute one my $self_path = abs_path($0); $self_path =~ s#/[^/]+$##; $self_path =~ s#/t/*$#/#; $target .= qq#"| cd $self_path && ./sender.pl --inbox='$list'"#; # remove hostname from email to make Postfix's postalias happy $email =~ s/@.+// if (not $self->{full_hostname_in_aliases}); if ($a->exists($email)) { $a->update($email, $target) or croak "can't update alias ".$a->error_check; } else { $a->append($email, $target) or croak "can't add alias ".$a->error_check; } # $a->write($aliases) or croak "can't save aliases $aliases ".$a->error_check; return 1; } =head2 _add_list Create new list my $list_obj = $nos->_add_list( list => 'My list', from => 'Outgoing from comment', email => 'my-list@example.com', aliases => '/etc/mail/mylist', ); Returns C object for created list. C address can be with domain or without it if your MTA appends it. There is no checking for validity of your list e-mail. Flexibility comes with resposibility, so please feed correct (and configured) return addresses. =cut sub _add_list { my $self = shift; my $arg = {@_}; my $name = lc($arg->{'list'}) || confess "can't add list without name"; my $email = lc($arg->{'email'}) || confess "can't add list without e-mail"; my $aliases = lc($arg->{'aliases'}) || confess "can't add list without path to aliases file"; my $from_addr = $arg->{'from'}; my $lists = $self->{'loader'}->find_class('lists'); $self->_add_aliases( list => $name, email => $email, aliases => $aliases, ) || warn "can't add alias $email for list $name"; my $l = $lists->find_or_create({ name => $name, email => $email, }); croak "can't add list $name\n" unless ($l); if ($from_addr && $l->from_addr ne $from_addr) { $l->from_addr($from_addr); $l->update; } $l->dbi_commit; return $l; } =head2 _get_list Get list C object. my $list_obj = $nos->check_list('My list'); Returns false on failure. =cut sub _get_list { my $self = shift; my $name = shift || return; my $lists = $self->{'loader'}->find_class('lists') || confess "can't find lists class"; return $lists->search({ name => lc($name) })->first; } =head2 _remove_alias Remove list alias my $ok = $nos->_remove_alias( email => 'mylist@example.com', aliases => '/etc/mail/mylist', ); Returns true if list is removed or false if list doesn't exist. Dies in case of error. =cut sub _remove_alias { my $self = shift; my $arg = {@_}; my $email = lc($arg->{'email'}) || confess "can't remove alias without email"; my $aliases = lc($arg->{'aliases'}) || confess "can't remove alias without list"; my $a = new Mail::Alias($aliases) || croak "can't open aliases file $aliases: $!"; if ($a->exists($email)) { $a->delete($email) || croak "can't remove alias $email"; } else { return 0; } return 1; } ### ### SOAP ### package Nos::SOAP; use Carp; =head1 SOAP methods This methods are thin wrappers to provide SOAP calls. They are grouped in C package which is in same F module file. Usually, you want to use named variables in your SOAP calls if at all possible. However, if you have broken SOAP library (like PHP SOAP class from PEAR) you will want to use positional arguments (in same order as documented for methods below). =cut my $nos; =head2 new Create new SOAP object my $soap = new Nos::SOAP( dsn => 'dbi:Pg:dbname=notices', user => 'dpavlin', passwd => '', debug => 1, verbose => 1, hash_len => 8, aliases => '/etc/aliases', ); If you are writing SOAP server (like C example), you will need to call this method once to make new instance of Nos::SOAP and specify C and options for it. =cut sub new { my $class = shift; my $self = {@_}; croak "need aliases parametar" unless ($self->{'aliases'}); bless($self, $class); $nos = new Nos( @_ ) || die "can't create Nos object"; $self ? return $self : return undef; } =head2 CreateList $message_id = CreateList( list => 'My list', from => 'Name of my list', email => 'my-list@example.com' ); =cut sub CreateList { my $self = shift; my $aliases = $nos->{'aliases'} || croak "need 'aliases' argument to new constructor"; if ($_[0] !~ m/^HASH/) { return $nos->create_list( list => $_[0], from => $_[1], email => $_[2], aliases => $aliases, ); } else { return $nos->create_list( %{ shift @_ }, aliases => $aliases ); } } =head2 DropList $ok = DropList( list => 'My list', ); =cut sub DropList { my $self = shift; my $aliases = $nos->{'aliases'} || croak "need 'aliases' argument to new constructor"; if ($_[0] !~ m/^HASH/) { return $nos->drop_list( list => $_[0], aliases => $aliases, ); } else { return $nos->drop_list( %{ shift @_ }, aliases => $aliases ); } } =head2 AddMemberToList $member_id = AddMemberToList( list => 'My list', email => 'e-mail@example.com', name => 'Full Name', ext_id => 42, ); =cut sub AddMemberToList { my $self = shift; if ($_[0] !~ m/^HASH/) { return $nos->add_member_to_list( list => $_[0], email => $_[1], name => $_[2], ext_id => $_[3], ); } else { return $nos->add_member_to_list( %{ shift @_ } ); } } =head2 ListMembers my @members = ListMembers( list => 'My list', ); Returns array of hashes with user informations, see C. =cut sub ListMembers { my $self = shift; my $list_name; if ($_[0] !~ m/^HASH/) { $list_name = shift; } else { $list_name = $_[0]->{'list'}; } return [ $nos->list_members( list => $list_name ) ]; } =head2 DeleteMemberFromList $member_id = DeleteMemberFromList( list => 'My list', email => 'e-mail@example.com', ); =cut sub DeleteMemberFromList { my $self = shift; if ($_[0] !~ m/^HASH/) { return $nos->delete_member_from_list( list => $_[0], email => $_[1], ); } else { return $nos->delete_member_from_list( %{ shift @_ } ); } } =head2 AddMessageToList $message_id = AddMessageToList( list => 'My list', message => 'From: My list...' ); =cut sub AddMessageToList { my $self = shift; if ($_[0] !~ m/^HASH/) { return $nos->add_message_to_list( list => $_[0], message => $_[1], ); } else { return $nos->add_message_to_list( %{ shift @_ } ); } } =head2 MessagesReceived Return statistics about received messages. my @result = MessagesReceived( list => 'My list', email => 'jdoe@example.com', from_date => '2005-01-01 10:15:00', to_date => '2005-01-01 12:00:00', message => 0, ); You must specify C or C or any combination of those two. Other parametars are optional. For format of returned array element see C. =cut sub MessagesReceived { my $self = shift; if ($_[0] !~ m/^HASH/) { die "need at least list or email" unless (scalar @_ < 2); return \@{ $nos->received_messages( list => $_[0], email => $_[1], from_date => $_[2], to_date => $_[3], message => $_[4] ) }; } else { my $arg = shift; die "need list or email argument" unless ($arg->{'list'} || $arg->{'email'}); return \@{ $nos->received_messages( %{ $arg } ) }; } } =head2 SendTest Internal function which does e-mail sending using C driver. my $sent = SendTest( list => 'My list' ); Returns number of messages sent =cut sub SendTest { my $self = shift; my $args = shift; die "list name required" unless ($args->{list}); require Email::Send::Test; my $nr_sent = $nos->send_queued_messages( list => $args->{list}, driver => 'Test', sleep => 0, verbose => 0, ); my @emails = Email::Send::Test->emails; open(my $tmp, ">/tmp/soap-debug"); use Data::Dump qw/dump/; print $tmp "sent $nr_sent emails\n", dump(@emails); close($tmp); return $nr_sent; } ### =head1 NOTE ON ARRAYS IN SOAP Returning arrays from SOAP calls is somewhat fuzzy (at least to me). It seems that SOAP::Lite client thinks that it has array with one element which is array of hashes with data. =head1 EXPORT Nothing. =head1 SEE ALSO mailman, ezmlm, sympa, L =head1 AUTHOR Dobrica Pavlinusic, Edpavlin@rot13.orgE =head1 COPYRIGHT AND LICENSE Copyright (C) 2005 by Dobrica Pavlinusic This library is free software; you can redistribute it and/or modify it under the same terms as Perl itself, either Perl version 5.8.4 or, at your option, any later version of Perl 5 you may have available. =cut 1;