/[notice-sender]/jifty-dbi/lib/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

Contents of /jifty-dbi/lib/Nos.pm

Parent Directory Parent Directory | Revision Log Revision Log


Revision 92 - (show annotations)
Tue Dec 19 10:32:18 2006 UTC (17 years, 5 months ago) by dpavlin
File size: 30097 byte(s)
begin migration from Class::DBI to Jifty::DBI
1 package Nos;
2
3 use 5.008;
4 use strict;
5 use warnings;
6
7 require Exporter;
8
9 our @ISA = qw(Exporter);
10
11 our %EXPORT_TAGS = ( 'all' => [ qw(
12 ) ] );
13
14 our @EXPORT_OK = ( @{ $EXPORT_TAGS{'all'} } );
15
16 our @EXPORT = qw(
17 );
18
19 our $VERSION = '0.9_00';
20
21 use Email::Valid;
22 use Email::Send;
23 use Carp;
24 use Email::Auth::AddressHash;
25 use Email::Simple;
26 use Email::Address;
27 use Mail::DeliveryStatus::BounceParser;
28 use Mail::Alias;
29 use Cwd qw(abs_path);
30
31 use Jifty::DBI::Handle;
32 use lib 'lib';
33 use Nos::Lists;
34
35
36 =head1 NAME
37
38 Nos - Notice Sender core module
39
40 =head1 SYNOPSIS
41
42 use Nos;
43 my $nos = new Nos();
44
45 =head1 DESCRIPTION
46
47 Notice sender is mail handler. It is not MTA, since it doesn't know how to
48 receive e-mails or send them directly to other hosts. It is not mail list
49 manager because it requires programming to add list members and send
50 messages. You can think of it as mechanisam for off-loading your e-mail
51 sending to remote server using SOAP service.
52
53 It's concept is based around B<lists>. Each list can have zero or more
54 B<members>. Each list can have zero or more B<messages>.
55
56 Here comes a twist: each outgoing message will have unique e-mail generated,
57 so Notice Sender will be able to link received replies (or bounces) with
58 outgoing messages.
59
60 It doesn't do much more than that. It B<can't> create MIME encoded e-mail,
61 send attachments, handle 8-bit characters in headers (which have to be
62 encoded) or anything else.
63
64 It will just queue your e-mail message to particular list (sending it to
65 possibly remote Notice Sender SOAP server just once), send it out at
66 reasonable rate (so that it doesn't flood your e-mail infrastructure) and
67 keep track replies.
68
69 It is best used to send small number of messages to more-or-less fixed
70 list of recipients while allowing individual responses to be examined.
71 Tipical use include replacing php e-mail sending code with SOAP call to
72 Notice Sender. It does support additional C<ext_id> field for each member
73 which can be used to track some unique identifier from remote system for
74 particular user.
75
76 It comes with command-line utility C<sender.pl> which can be used to perform
77 all available operation from scripts (see C<sender.pl --man>).
78 This command is also useful for debugging while writing client SOAP
79 application.
80
81 =head1 METHODS
82
83 =head2 new
84
85 Create new instance specifing database, user, password and options.
86
87 my $nos = new Nos(
88 dsn => 'dbi:Pg:dbname=notices',
89 user => 'dpavlin',
90 passwd => '',
91 debug => 1,
92 verbose => 1,
93 hash_len => 8,
94 full_hostname_in_aliases => 0,
95 );
96
97 Parametar C<hash_len> defines length of hash which will be added to each
98 outgoing e-mail message to ensure that replies can be linked with sent e-mails.
99
100 C<full_hostname_in_aliases> will turn on old behaviour (not supported by Postfix
101 postalias) to include full hostname in aliases file.
102
103
104 =cut
105
106 sub new {
107 my $class = shift;
108 my $self = {@_};
109 bless($self, $class);
110
111 croak "need at least dsn" unless ($self->{dsn});
112
113 my (undef,$driver,$dbname) = split(/:/, $self->{dsn});
114 $dbname =~ s!^dbname=!!;
115
116 $self->{h} = Jifty::DBI::Handle->new();
117 $self->{h}->connect(
118 driver => $driver,
119 database => $dbname,
120 host => 'localhost',
121 user => $self->{user},
122 password => $self->{passwd},
123 );
124
125 $self->{'loader'} = Class::DBI::Loader->new(
126 debug => $self->{'debug'},
127 dsn => $self->{'dsn'},
128 user => $self->{'user'},
129 password => $self->{'passwd'},
130 namespace => "Nos",
131 additional_classes => qw/Class::DBI::AbstractSearch/,
132 # additional_base_classes => qw/My::Stuff/,
133 relationships => 1,
134 ) || croak "can't init Class::DBI::Loader";
135
136 $self->{'hash_len'} ||= 8;
137
138 $self ? return $self : return undef;
139 }
140
141
142 =head2 create_list
143
144 Create new list. Required arguments are name of C<list>, C<email> address
145 and path to C<aliases> file.
146
147 $nos->create_list(
148 list => 'My list',
149 from => 'Outgoing from comment',
150 email => 'my-list@example.com',
151 aliases => '/etc/mail/mylist',
152 archive => '/path/to/mbox/archive',
153 );
154
155 Returns ID of newly created list.
156
157 Calls internally C<_add_list>, see details there.
158
159 =cut
160
161 sub create_list {
162 my $self = shift;
163
164 my $arg = {@_};
165
166 confess "need list name" unless ($arg->{'list'});
167 confess "need list email" unless ($arg->{'email'});
168
169 $arg->{'list'} = lc($arg->{'list'});
170 $arg->{'email'} = lc($arg->{'email'});
171
172 my $l = $self->_get_list($arg->{'list'}) ||
173 $self->_add_list( @_ ) ||
174 return undef;
175
176 return $l->id;
177 }
178
179
180 =head2 drop_list
181
182 Delete list from database.
183
184 my $ok = drop_list(
185 list => 'My list'
186 aliases => '/etc/mail/mylist',
187 );
188
189 Returns false if list doesn't exist.
190
191 =cut
192
193 sub drop_list {
194 my $self = shift;
195
196 my $args = {@_};
197
198 croak "need list to delete" unless ($args->{'list'});
199
200 $args->{'list'} = lc($args->{'list'});
201
202 my $aliases = $args->{'aliases'} || croak "need path to aliases file";
203
204 my $lists = $self->{'loader'}->find_class('lists');
205
206 my $this_list = $lists->search( name => $args->{'list'} )->first || return;
207
208 $self->_remove_alias( email => $this_list->email, aliases => $aliases);
209
210 $this_list->delete || croak "can't delete list\n";
211
212 return $lists->dbi_commit || croak "can't commit";
213 }
214
215
216 =head2 add_member_to_list
217
218 Add new member to list
219
220 $nos->add_member_to_list(
221 list => "My list",
222 email => "john.doe@example.com",
223 name => "John A. Doe",
224 ext_id => 42,
225 );
226
227 C<name> and C<ext_id> parametars are optional.
228
229 Return member ID if user is added.
230
231 =cut
232
233 sub add_member_to_list {
234 my $self = shift;
235
236 my $arg = {@_};
237
238 my $email = lc($arg->{'email'}) || croak "can't add user without e-mail";
239 my $name = $arg->{'name'} || '';
240 my $list_name = lc($arg->{'list'}) || croak "need list name";
241 my $ext_id = $arg->{'ext_id'};
242
243 my $list = $self->_get_list($list_name) || croak "list $list_name doesn't exist";
244
245 if (! Email::Valid->address($email)) {
246 carp "SKIPPING $name <$email>\n";
247 return 0;
248 }
249
250 carp "# $name <$email>\n" if ($self->{'verbose'});
251
252 my $users = $self->{'loader'}->find_class('users');
253 my $user_list = $self->{'loader'}->find_class('user_list');
254
255 my $this_user = $users->find_or_create({
256 email => $email,
257 }) || croak "can't find or create member\n";
258
259 if ($name && $this_user->name ne $name) {
260 $this_user->name($name || '');
261 $this_user->update;
262 }
263
264 if (defined($ext_id) && ($this_user->ext_id || '') ne $ext_id) {
265 $this_user->ext_id($ext_id);
266 $this_user->update;
267 }
268
269 my $user_on_list = $user_list->find_or_create({
270 user_id => $this_user->id,
271 list_id => $list->id,
272 }) || croak "can't add user to list";
273
274 $list->dbi_commit;
275 $this_user->dbi_commit;
276 $user_on_list->dbi_commit;
277
278 return $this_user->id;
279 }
280
281 =head2 list_members
282
283 List all members of some list.
284
285 my @members = list_members(
286 list => 'My list',
287 );
288
289 Returns array of hashes with user information like this:
290
291 $member = {
292 name => 'Dobrica Pavlinusic',
293 email => 'dpavlin@rot13.org
294 }
295
296 If list is not found, returns false. If there is C<ext_id> in user data,
297 it will also be returned.
298
299 =cut
300
301 sub list_members {
302 my $self = shift;
303
304 my $args = {@_};
305
306 my $list_name = lc($args->{'list'}) || confess "need list name";
307
308 my $lists = $self->{'loader'}->find_class('lists');
309 my $user_list = $self->{'loader'}->find_class('user_list');
310
311 my $this_list = $lists->search( name => $list_name )->first || return;
312
313 my @results;
314
315 foreach my $user_on_list ($user_list->search(list_id => $this_list->id)) {
316 my $row = {
317 name => $user_on_list->user_id->name,
318 email => $user_on_list->user_id->email,
319 };
320
321 my $ext_id = $user_on_list->user_id->ext_id;
322 $row->{'ext_id'} = $ext_id if (defined($ext_id));
323
324 push @results, $row;
325 }
326
327 return @results;
328
329 }
330
331
332 =head2 delete_member
333
334 Delete member from database.
335
336 my $ok = delete_member(
337 name => 'Dobrica Pavlinusic'
338 );
339
340 my $ok = delete_member(
341 email => 'dpavlin@rot13.org'
342 );
343
344 Returns false if user doesn't exist.
345
346 This function will delete member from all lists (by cascading delete), so it
347 shouldn't be used lightly.
348
349 =cut
350
351 sub delete_member {
352 my $self = shift;
353
354 my $args = {@_};
355
356 croak "need name or email of user to delete" unless ($args->{'name'} || $args->{'email'});
357
358 $args->{'email'} = lc($args->{'email'}) if ($args->{'email'});
359
360 my $key = 'name';
361 $key = 'email' if ($args->{'email'});
362
363 my $users = $self->{'loader'}->find_class('users');
364
365 my $this_user = $users->search( $key => $args->{$key} )->first || return;
366
367 $this_user->delete || croak "can't delete user\n";
368
369 return $users->dbi_commit || croak "can't commit";
370 }
371
372 =head2 delete_member_from_list
373
374 Delete member from particular list.
375
376 my $ok = delete_member_from_list(
377 list => 'My list',
378 email => 'dpavlin@rot13.org',
379 );
380
381 Returns false if user doesn't exist on that particular list.
382
383 It will die if list or user doesn't exist. You have been warned (you might
384 want to eval this functon to prevent it from croaking).
385
386 =cut
387
388 sub delete_member_from_list {
389 my $self = shift;
390
391 my $args = {@_};
392
393 croak "need list name and email of user to delete" unless ($args->{'list'} && $args->{'email'});
394
395 $args->{'list'} = lc($args->{'list'});
396 $args->{'email'} = lc($args->{'email'});
397
398 my $user = $self->{'loader'}->find_class('users');
399 my $list = $self->{'loader'}->find_class('lists');
400 my $user_list = $self->{'loader'}->find_class('user_list');
401
402 my $this_user = $user->search( email => $args->{'email'} )->first || croak "can't find user: ".$args->{'email'};
403 my $this_list = $list->search( name => $args->{'list'} )->first || croak "can't find list: ".$args->{'list'};
404
405 my $this_user_list = $user_list->search_where( list_id => $this_list->id, user_id => $this_user->id )->first || return;
406
407 $this_user_list->delete || croak "can't delete user from list\n";
408
409 return $user_list->dbi_commit || croak "can't commit";
410 }
411
412 =head2 add_message_to_list
413
414 Adds message to one list's queue for later sending.
415
416 $nos->add_message_to_list(
417 list => 'My list',
418 message => 'Subject: welcome to list
419
420 This is example message
421 ',
422 );
423
424 On success returns ID of newly created (or existing) message.
425
426 Only required header in e-mail is C<Subject:>. C<From:> and C<To:> headers
427 will be automatically generated, but if you want to use own headers, just
428 include them in messages.
429
430 =cut
431
432 sub add_message_to_list {
433 my $self = shift;
434
435 my $args = {@_};
436
437 my $list_name = lc($args->{'list'}) || confess "need list name";
438 my $message_text = $args->{'message'} || croak "need message";
439
440 my $m = Email::Simple->new($message_text) || croak "can't parse message";
441
442 warn "message doesn't have Subject header\n" unless( $m->header('Subject') );
443
444 my $lists = $self->{'loader'}->find_class('lists');
445
446 my $this_list = $lists->search(
447 name => $list_name,
448 )->first || croak "can't find list $list_name";
449
450 my $messages = $self->{'loader'}->find_class('messages');
451
452 my $this_message = $messages->find_or_create({
453 message => $message_text
454 }) || croak "can't insert message";
455
456 $this_message->dbi_commit() || croak "can't add message";
457
458 my $queue = $self->{'loader'}->find_class('queue');
459
460 $queue->find_or_create({
461 message_id => $this_message->id,
462 list_id => $this_list->id,
463 }) || croak "can't add message ",$this_message->id," to list ",$this_list->id, ": ",$this_list->name;
464
465 $queue->dbi_commit || croak "can't add message to list ",$this_list->name;
466
467 return $this_message->id;
468 }
469
470
471 =head2 send_queued_messages
472
473 Send queued messages or just ones for selected list
474
475 $nos->send_queued_messages(
476 list => 'My list',
477 driver => 'smtp',
478 sleep => 3,
479 );
480
481 Second option is driver which will be used for e-mail delivery. If not
482 specified, C<IO> driver will be used which will dump e-mail to C<STDERR>.
483
484 Other valid drivers are:
485
486 =over 10
487
488 =item smtp
489
490 Send e-mail using SMTP server at 127.0.0.1
491
492 =back
493
494 Any other driver name will try to use C<Email::Send::that_driver> module.
495
496 Default sleep wait between two messages is 3 seconds.
497
498 This method will return number of succesfully sent messages.
499
500 =cut
501
502 sub send_queued_messages {
503 my $self = shift;
504
505 my $arg = {@_};
506
507 my $list_name = lc($arg->{'list'}) || '';
508 my $driver = $arg->{'driver'} || '';
509 my $sleep = $arg->{'sleep'};
510 $sleep ||= 3 unless defined($sleep);
511
512 # number of messages sent o.k.
513 my $ok = 0;
514
515 my $email_send_driver = 'Email::Send::IO';
516 my @email_send_options;
517
518 if (lc($driver) eq 'smtp') {
519 $email_send_driver = 'Email::Send::SMTP';
520 @email_send_options = ['127.0.0.1'];
521 } elsif ($driver && $driver ne '') {
522 $email_send_driver = 'Email::Send::' . $driver;
523 } else {
524 warn "dumping all messages to STDERR\n";
525 }
526
527 my $lists = $self->{'loader'}->find_class('lists');
528 my $queue = $self->{'loader'}->find_class('queue');
529 my $user_list = $self->{'loader'}->find_class('user_list');
530 my $sent = $self->{'loader'}->find_class('sent');
531
532 my $my_q;
533 if ($list_name ne '') {
534 my $l_id = $lists->search_like( name => $list_name )->first ||
535 croak "can't find list $list_name";
536 $my_q = $queue->search_like( list_id => $l_id ) ||
537 croak "can't find list $list_name";
538 } else {
539 $my_q = $queue->retrieve_all;
540 }
541
542 while (my $m = $my_q->next) {
543 next if ($m->all_sent);
544
545 print "sending message ",$m->message_id," enqueued on ",$m->date," to list ",$m->list_id->name,"\n";
546 my $msg = $m->message_id->message;
547
548 foreach my $u ($user_list->search(list_id => $m->list_id)) {
549
550 my $to_email = $u->user_id->email;
551
552 my ($from,$domain) = split(/@/, $u->list_id->email, 2);
553
554 if ($sent->search( message_id => $m->message_id, user_id => $u->user_id )) {
555 print "SKIP $to_email message allready sent\n";
556 } else {
557 print "=> $to_email ";
558
559 my $secret = $m->list_id->name . " " . $u->user_id->email . " " . $m->message_id;
560 my $auth = Email::Auth::AddressHash->new( $secret, $self->{'hash_len'} );
561
562 my $hash = $auth->generate_hash( $to_email );
563
564 my $from_addr;
565 my $from_email_only = $from . "+" . $hash . ( $domain ? '@' . $domain : '');
566
567 $from_addr .= '"' . $u->list_id->from_addr . '" ' if ($u->list_id->from_addr);
568 $from_addr .= '<' . $from_email_only . '>';
569 my $to = '"' . $u->user_id->name . '" <' . $to_email . '>';
570
571 my $m_obj = Email::Simple->new($msg) || croak "can't parse message";
572
573 $m_obj->header_set('Return-Path', $from_email_only) || croak "can't set Return-Path: header";
574 #$m_obj->header_set('Sender', $from_email_only) || croak "can't set Sender: header";
575 $m_obj->header_set('Errors-To', $from_email_only) || croak "can't set Errors-To: header";
576 $m_obj->header_set('From', $from_addr) || croak "can't set From: header";
577 $m_obj->header_set('To', $to) || croak "can't set To: header";
578
579 $m_obj->header_set('X-Nos-Version', $VERSION);
580 $m_obj->header_set('X-Nos-Hash', $hash);
581
582 # really send e-mail
583 my $sent_status;
584
585 if (@email_send_options) {
586 $sent_status = send $email_send_driver => $m_obj->as_string, @email_send_options;
587 } else {
588 $sent_status = send $email_send_driver => $m_obj->as_string;
589 }
590
591 croak "can't send e-mail: $sent_status\n\nOriginal e-mail follows:\n".$m_obj->as_string unless ($sent_status);
592 my @bad;
593 @bad = @{ $sent_status->prop('bad') } if (eval { $sent_status->can('prop') });
594 croak "failed sending to ",join(",",@bad) if (@bad);
595
596 if ($sent_status) {
597
598 $sent->create({
599 message_id => $m->message_id,
600 user_id => $u->user_id,
601 hash => $hash,
602 });
603 $sent->dbi_commit;
604
605 print " - $sent_status\n";
606
607 $ok++;
608 } else {
609 warn "ERROR: $sent_status\n";
610 }
611
612 if ($sleep) {
613 warn "sleeping $sleep seconds\n";
614 sleep($sleep);
615 }
616 }
617 }
618 $m->all_sent(1);
619 $m->update;
620 $m->dbi_commit;
621 }
622
623 return $ok;
624
625 }
626
627 =head2 inbox_message
628
629 Receive single message for list's inbox.
630
631 my $ok = $nos->inbox_message(
632 list => 'My list',
633 message => $message,
634 );
635
636 This method is used by C<sender.pl> when receiving e-mail messages.
637
638 =cut
639
640 sub inbox_message {
641 my $self = shift;
642
643 my $arg = {@_};
644
645 return unless ($arg->{'message'});
646 croak "need list name" unless ($arg->{'list'});
647
648 $arg->{'list'} = lc($arg->{'list'});
649
650 my $this_list = $self->_get_list($arg->{'list'}) || croak "can't find list ".$arg->{'list'}."\n";
651
652 my $m = Email::Simple->new($arg->{'message'}) || croak "can't parse message";
653
654 my $to = $m->header('To') || die "can't find To: address in incomming message\n";
655
656 my $return_path = $m->header('Return-Path') || '';
657
658 my @addrs = Email::Address->parse( $to );
659
660 die "can't parse To: $to address\n" unless (@addrs);
661
662 my $hl = $self->{'hash_len'} || confess "no hash_len?";
663
664 my $hash;
665
666 foreach my $a (@addrs) {
667 if ($a->address =~ m/\+([a-f0-9]{$hl})@/i) {
668 $hash = $1;
669 last;
670 }
671 }
672
673 #warn "can't find hash in e-mail $to\n" unless ($hash);
674
675 my $sent = $self->{'loader'}->find_class('sent');
676
677 # will use null if no matching message_id is found
678 my $sent_msg;
679 $sent_msg = $sent->search( hash => $hash )->first if ($hash);
680
681 my ($message_id, $user_id) = (undef, undef); # init with NULL
682
683 if ($sent_msg) {
684 $message_id = $sent_msg->message_id || carp "no message_id";
685 $user_id = $sent_msg->user_id || carp "no user_id";
686 } else {
687 #warn "can't find sender with hash $hash\n";
688 my $users = $self->{'loader'}->find_class('users');
689 my $from = $m->header('From');
690 $from = $1 if ($from =~ m/<(.*)>/);
691 my $this_user = $users->search( email => lc($from) )->first;
692 $user_id = $this_user->id if ($this_user);
693 }
694
695
696 my $is_bounce = 0;
697
698 if ($return_path eq '<>' || $return_path eq '') {
699 no warnings;
700 my $bounce = eval { Mail::DeliveryStatus::BounceParser->new(
701 $arg->{'message'}, { report_non_bounces=>1 },
702 ) };
703 #warn "can't check if this message is bounce!" if ($@);
704
705 $is_bounce++ if ($bounce && $bounce->is_bounce);
706 }
707
708 my $received = $self->{'loader'}->find_class('received');
709
710 my $this_received = $received->find_or_create({
711 user_id => $user_id,
712 list_id => $this_list->id,
713 message_id => $message_id,
714 message => $arg->{'message'},
715 bounced => $is_bounce,
716 }) || croak "can't insert received message";
717
718 $this_received->dbi_commit;
719
720 # print "message_id: ",($message_id || "not found")," -- $is_bounce\n";
721 }
722
723 =head2 received_messages
724
725 Returns all received messages for given list or user.
726
727 my @received = $nos->received_messages(
728 list => 'My list',
729 email => "john.doe@example.com",
730 from_date => '2005-01-01 10:15:00',
731 to_date => '2005-01-01 12:00:00',
732 message => 0,
733 );
734
735 If don't specify C<list> or C<email> it will return all received messages.
736 Results will be sorted by received date, oldest first.
737
738 Other optional parametars include:
739
740 =over 10
741
742 =item from_date
743
744 Date (in ISO format) for lower limit of dates received
745
746 =item to_date
747
748 Return just messages older than this date
749
750 =item message
751
752 Include whole received message in result. This will probably make result
753 array very large. Use with care.
754
755 =back
756
757 Date ranges are inclusive, so results will include messages sent on
758 particular date specified with C<date_from> or C<date_to>.
759
760 Each element in returned array will have following structure:
761
762 my $row = {
763 id => 42, # unique ID of received message
764 list => 'My list', # useful if filtering by email
765 ext_id => 9999, # ext_id from message sender
766 email => 'jdoe@example.com', # e-mail of message sender
767 bounced => 0, # true if message is bounce
768 date => '2005-08-24 18:57:24', # date of receival in ISO format
769 }
770
771 If you specified C<message> option, this hash will also have C<message> key
772 which will contain whole received message.
773
774 =cut
775
776 sub received_messages {
777 my $self = shift;
778
779 my $arg = {@_} if (@_);
780
781 # croak "need list name or email" unless ($arg->{'list'} || $arg->{'email'});
782
783 my $sql = qq{
784 select
785 received.id as id,
786 lists.name as list,
787 users.ext_id as ext_id,
788 users.email as email,
789 };
790 $sql .= qq{ message,} if ($arg->{'message'});
791 $sql .= qq{
792 bounced,received.date as date
793 from received
794 join lists on lists.id = list_id
795 join users on users.id = user_id
796 };
797
798 my $order = qq{ order by date asc };
799
800 my $where;
801
802 $where->{'lists.name'} = lc($arg->{'list'}) if ($arg->{'list'});
803 $where->{'users.email'} = lc($arg->{'email'}) if ($arg->{'email'});
804 $where->{'received.date'} = { '>=', $arg->{'date_from'} } if ($arg->{'date_from'});
805 $where->{'received.date'} = { '<=', $arg->{'date_to'} } if ($arg->{'date_to'});
806
807 # hum, yammy one-liner
808 my($stmt, @bind) = SQL::Abstract->new->where($where);
809
810 my $dbh = $self->{'loader'}->find_class('received')->db_Main;
811
812 my $sth = $dbh->prepare($sql . $stmt . $order);
813 $sth->execute(@bind);
814 return $sth->fetchall_hash;
815 }
816
817
818 =head1 INTERNAL METHODS
819
820 Beware of dragons! You shouldn't need to call those methods directly.
821
822
823 =head2 _add_aliases
824
825 Add or update alias in C</etc/aliases> (or equivalent) file for selected list
826
827 my $ok = $nos->add_aliases(
828 list => 'My list',
829 email => 'my-list@example.com',
830 aliases => '/etc/mail/mylist',
831 archive => '/path/to/mbox/archive',
832
833 );
834
835 C<archive> parametar is optional.
836
837 Return false on failure.
838
839 =cut
840
841 sub _add_aliases {
842 my $self = shift;
843
844 my $arg = {@_};
845
846 foreach my $o (qw/list email aliases/) {
847 croak "need $o option" unless ($arg->{$o});
848 }
849
850 my $aliases = $arg->{'aliases'};
851 my $email = $arg->{'email'};
852 my $list = $arg->{'list'};
853
854 unless (-e $aliases) {
855 warn "aliases file $aliases doesn't exist, creating empty\n";
856 open(my $fh, '>', $aliases) || croak "can't create $aliases: $!";
857 close($fh);
858 chmod 0777, $aliases || warn "can't change permission to 0777";
859 }
860
861 die "FATAL: aliases file $aliases is not writable\n" unless (-w $aliases);
862
863 my $a = new Mail::Alias($aliases) || croak "can't open aliases file $aliases: $!";
864
865 my $target = '';
866
867 if (my $archive = $arg->{'archive'}) {
868 $target .= "$archive, ";
869
870 if (! -e $archive) {
871 warn "please make sure that file $archive is writable for your e-mail user (defaulting to bad 777 permission for now)";
872
873 open(my $fh, '>', $archive) || croak "can't create archive file $archive: $!";
874 close($fh);
875 chmod 0777, $archive || croak "can't chmod archive file $archive to 0777: $!";
876 }
877 }
878
879 # resolve my path to absolute one
880 my $self_path = abs_path($0);
881 $self_path =~ s#/[^/]+$##;
882 $self_path =~ s#/t/*$#/#;
883
884 $target .= qq#"| cd $self_path && ./sender.pl --inbox='$list'"#;
885
886 # remove hostname from email to make Postfix's postalias happy
887 $email =~ s/@.+// if (not $self->{full_hostname_in_aliases});
888
889 if ($a->exists($email)) {
890 $a->update($email, $target) or croak "can't update alias ".$a->error_check;
891 } else {
892 $a->append($email, $target) or croak "can't add alias ".$a->error_check;
893 }
894
895 # $a->write($aliases) or croak "can't save aliases $aliases ".$a->error_check;
896
897 return 1;
898 }
899
900 =head2 _add_list
901
902 Create new list
903
904 my $list_obj = $nos->_add_list(
905 list => 'My list',
906 from => 'Outgoing from comment',
907 email => 'my-list@example.com',
908 aliases => '/etc/mail/mylist',
909 );
910
911 Returns C<Class::DBI> object for created list.
912
913 C<email> address can be with domain or without it if your
914 MTA appends it. There is no checking for validity of your
915 list e-mail. Flexibility comes with resposibility, so please
916 feed correct (and configured) return addresses.
917
918 =cut
919
920 sub find_or_create {
921 my $self = shift;
922 my $obj = shift;
923 my %args = {@_};
924
925 my ( $id, $msg ) = $obj->load_by_cols(%args);
926 unless ( $obj->{values}->{id} ) {
927 warn "find_or_CREATE(",dump( \%args ), ")";
928 return $obj->create(%args);
929 }
930
931 warn "FIND_or_create(",dump( \%args ), ") = $id";
932 return $id;
933 }
934
935
936 sub _add_list {
937 my $self = shift;
938
939 my $arg = {@_};
940
941 my $name = lc($arg->{'list'}) || confess "can't add list without name";
942 my $email = lc($arg->{'email'}) || confess "can't add list without e-mail";
943 my $aliases = lc($arg->{'aliases'}) || confess "can't add list without path to aliases file";
944
945 my $from_addr = $arg->{'from'};
946
947 my $lists = Nos::Lists->new( handle => $self->{h} );
948
949 $self->_add_aliases(
950 list => $name,
951 email => $email,
952 aliases => $aliases,
953 ) || warn "can't add alias $email for list $name";
954
955 my $l = $self->find_or_create($lists, {
956 name => $name,
957 email => $email,
958 });
959
960 croak "can't add list $name\n" unless ($l);
961
962 if ($from_addr && $l->from_addr ne $from_addr) {
963 $l->from_addr($from_addr);
964 $l->update;
965 }
966
967 $l->dbi_commit;
968
969 return $l;
970
971 }
972
973
974
975 =head2 _get_list
976
977 Get list C<Class::DBI> object.
978
979 my $list_obj = $nos->check_list('My list');
980
981 Returns false on failure.
982
983 =cut
984
985 sub _get_list {
986 my $self = shift;
987
988 my $name = shift || return;
989
990 my $lists = $self->{'loader'}->find_class('lists') || confess "can't find lists class";
991
992 return $lists->search({ name => lc($name) })->first;
993 }
994
995
996 =head2 _remove_alias
997
998 Remove list alias
999
1000 my $ok = $nos->_remove_alias(
1001 email => 'mylist@example.com',
1002 aliases => '/etc/mail/mylist',
1003 );
1004
1005 Returns true if list is removed or false if list doesn't exist. Dies in case of error.
1006
1007 =cut
1008
1009 sub _remove_alias {
1010 my $self = shift;
1011
1012 my $arg = {@_};
1013
1014 my $email = lc($arg->{'email'}) || confess "can't remove alias without email";
1015 my $aliases = lc($arg->{'aliases'}) || confess "can't remove alias without list";
1016
1017 my $a = new Mail::Alias($aliases) || croak "can't open aliases file $aliases: $!";
1018
1019 if ($a->exists($email)) {
1020 $a->delete($email) || croak "can't remove alias $email";
1021 } else {
1022 return 0;
1023 }
1024
1025 return 1;
1026
1027 }
1028
1029 ###
1030 ### SOAP
1031 ###
1032
1033 package Nos::SOAP;
1034
1035 use Carp;
1036
1037 =head1 SOAP methods
1038
1039 This methods are thin wrappers to provide SOAP calls. They are grouped in
1040 C<Nos::SOAP> package which is in same F<Nos.pm> module file.
1041
1042 Usually, you want to use named variables in your SOAP calls if at all
1043 possible.
1044
1045 However, if you have broken SOAP library (like PHP SOAP class from PEAR)
1046 you will want to use positional arguments (in same order as documented for
1047 methods below).
1048
1049 =cut
1050
1051 my $nos;
1052
1053
1054 =head2 new
1055
1056 Create new SOAP object
1057
1058 my $soap = new Nos::SOAP(
1059 dsn => 'dbi:Pg:dbname=notices',
1060 user => 'dpavlin',
1061 passwd => '',
1062 debug => 1,
1063 verbose => 1,
1064 hash_len => 8,
1065 aliases => '/etc/aliases',
1066 );
1067
1068 If you are writing SOAP server (like C<soap.cgi> example), you will need to
1069 call this method once to make new instance of Nos::SOAP and specify C<dsn>
1070 and options for it.
1071
1072 =cut
1073
1074 sub new {
1075 my $class = shift;
1076 my $self = {@_};
1077
1078 croak "need aliases parametar" unless ($self->{'aliases'});
1079
1080 bless($self, $class);
1081
1082 $nos = new Nos( @_ ) || die "can't create Nos object";
1083
1084 $self ? return $self : return undef;
1085 }
1086
1087
1088 =head2 CreateList
1089
1090 $message_id = CreateList(
1091 list => 'My list',
1092 from => 'Name of my list',
1093 email => 'my-list@example.com'
1094 );
1095
1096 =cut
1097
1098 sub CreateList {
1099 my $self = shift;
1100
1101 my $aliases = $nos->{'aliases'} || croak "need 'aliases' argument to new constructor";
1102
1103 if ($_[0] !~ m/^HASH/) {
1104 return $nos->create_list(
1105 list => $_[0], from => $_[1], email => $_[2],
1106 aliases => $aliases,
1107 );
1108 } else {
1109 return $nos->create_list( %{ shift @_ }, aliases => $aliases );
1110 }
1111 }
1112
1113
1114 =head2 DropList
1115
1116 $ok = DropList(
1117 list => 'My list',
1118 );
1119
1120 =cut
1121
1122 sub DropList {
1123 my $self = shift;
1124
1125 my $aliases = $nos->{'aliases'} || croak "need 'aliases' argument to new constructor";
1126
1127 if ($_[0] !~ m/^HASH/) {
1128 return $nos->drop_list(
1129 list => $_[0],
1130 aliases => $aliases,
1131 );
1132 } else {
1133 return $nos->drop_list( %{ shift @_ }, aliases => $aliases );
1134 }
1135 }
1136
1137 =head2 AddMemberToList
1138
1139 $member_id = AddMemberToList(
1140 list => 'My list',
1141 email => 'e-mail@example.com',
1142 name => 'Full Name',
1143 ext_id => 42,
1144 );
1145
1146 =cut
1147
1148 sub AddMemberToList {
1149 my $self = shift;
1150
1151 if ($_[0] !~ m/^HASH/) {
1152 return $nos->add_member_to_list(
1153 list => $_[0], email => $_[1], name => $_[2], ext_id => $_[3],
1154 );
1155 } else {
1156 return $nos->add_member_to_list( %{ shift @_ } );
1157 }
1158 }
1159
1160
1161 =head2 ListMembers
1162
1163 my @members = ListMembers(
1164 list => 'My list',
1165 );
1166
1167 Returns array of hashes with user informations, see C<list_members>.
1168
1169 =cut
1170
1171 sub ListMembers {
1172 my $self = shift;
1173
1174 my $list_name;
1175
1176 if ($_[0] !~ m/^HASH/) {
1177 $list_name = shift;
1178 } else {
1179 $list_name = $_[0]->{'list'};
1180 }
1181
1182 return [ $nos->list_members( list => $list_name ) ];
1183 }
1184
1185
1186 =head2 DeleteMemberFromList
1187
1188 $member_id = DeleteMemberFromList(
1189 list => 'My list',
1190 email => 'e-mail@example.com',
1191 );
1192
1193 =cut
1194
1195 sub DeleteMemberFromList {
1196 my $self = shift;
1197
1198 if ($_[0] !~ m/^HASH/) {
1199 return $nos->delete_member_from_list(
1200 list => $_[0], email => $_[1],
1201 );
1202 } else {
1203 return $nos->delete_member_from_list( %{ shift @_ } );
1204 }
1205 }
1206
1207
1208 =head2 AddMessageToList
1209
1210 $message_id = AddMessageToList(
1211 list => 'My list',
1212 message => 'From: My list...'
1213 );
1214
1215 =cut
1216
1217 sub AddMessageToList {
1218 my $self = shift;
1219
1220 if ($_[0] !~ m/^HASH/) {
1221 return $nos->add_message_to_list(
1222 list => $_[0], message => $_[1],
1223 );
1224 } else {
1225 return $nos->add_message_to_list( %{ shift @_ } );
1226 }
1227 }
1228
1229 =head2 MessagesReceived
1230
1231 Return statistics about received messages.
1232
1233 my @result = MessagesReceived(
1234 list => 'My list',
1235 email => 'jdoe@example.com',
1236 from_date => '2005-01-01 10:15:00',
1237 to_date => '2005-01-01 12:00:00',
1238 message => 0,
1239 );
1240
1241 You must specify C<list> or C<email> or any combination of those two. Other
1242 parametars are optional.
1243
1244 For format of returned array element see C<received_messages>.
1245
1246 =cut
1247
1248 sub MessagesReceived {
1249 my $self = shift;
1250
1251 if ($_[0] !~ m/^HASH/) {
1252 die "need at least list or email" unless (scalar @_ < 2);
1253 return \@{ $nos->received_messages(
1254 list => $_[0], email => $_[1],
1255 from_date => $_[2], to_date => $_[3],
1256 message => $_[4]
1257 ) };
1258 } else {
1259 my $arg = shift;
1260 die "need list or email argument" unless ($arg->{'list'} || $arg->{'email'});
1261 return \@{ $nos->received_messages( %{ $arg } ) };
1262 }
1263 }
1264
1265 ###
1266
1267 =head1 NOTE ON ARRAYS IN SOAP
1268
1269 Returning arrays from SOAP calls is somewhat fuzzy (at least to me). It
1270 seems that SOAP::Lite client thinks that it has array with one element which
1271 is array of hashes with data.
1272
1273 =head1 PRIVATE METHODS
1274
1275 Documented here because tests use them
1276
1277 =head2 _nos_object
1278
1279 my $nos = $nos->_nos_object;
1280
1281 =cut
1282
1283 sub _nos_object {
1284 return $nos;
1285 }
1286
1287 =head1 EXPORT
1288
1289 Nothing.
1290
1291 =head1 SEE ALSO
1292
1293 mailman, ezmlm, sympa, L<Mail::Salsa>
1294
1295
1296 =head1 AUTHOR
1297
1298 Dobrica Pavlinusic, E<lt>dpavlin@rot13.orgE<gt>
1299
1300
1301 =head1 COPYRIGHT AND LICENSE
1302
1303 Copyright (C) 2005 by Dobrica Pavlinusic
1304
1305 This library is free software; you can redistribute it and/or modify
1306 it under the same terms as Perl itself, either Perl version 5.8.4 or,
1307 at your option, any later version of Perl 5 you may have available.
1308
1309
1310 =cut
1311
1312 1;

  ViewVC Help
Powered by ViewVC 1.1.26