/[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

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

Parent Directory Parent Directory | Revision Log Revision Log


Revision 92 - (hide 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 dpavlin 20 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 dpavlin 92 our $VERSION = '0.9_00';
20 dpavlin 20
21     use Email::Valid;
22     use Email::Send;
23     use Carp;
24 dpavlin 29 use Email::Auth::AddressHash;
25     use Email::Simple;
26 dpavlin 36 use Email::Address;
27 dpavlin 37 use Mail::DeliveryStatus::BounceParser;
28 dpavlin 66 use Mail::Alias;
29     use Cwd qw(abs_path);
30 dpavlin 20
31 dpavlin 92 use Jifty::DBI::Handle;
32     use lib 'lib';
33     use Nos::Lists;
34 dpavlin 47
35 dpavlin 92
36 dpavlin 20 =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 dpavlin 60 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 dpavlin 20
53 dpavlin 60 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 dpavlin 74 keep track replies.
68 dpavlin 60
69 dpavlin 74 It is best used to send small number of messages to more-or-less fixed
70 dpavlin 60 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 dpavlin 74 all available operation from scripts (see C<sender.pl --man>).
78 dpavlin 60 This command is also useful for debugging while writing client SOAP
79     application.
80    
81 dpavlin 20 =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 dpavlin 36 hash_len => 8,
94 dpavlin 89 full_hostname_in_aliases => 0,
95 dpavlin 20 );
96    
97 dpavlin 38 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 dpavlin 36
100 dpavlin 89 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 dpavlin 20 =cut
105    
106     sub new {
107 dpavlin 89 my $class = shift;
108     my $self = {@_};
109 dpavlin 20 bless($self, $class);
110    
111 dpavlin 92 croak "need at least dsn" unless ($self->{dsn});
112 dpavlin 22
113 dpavlin 92 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 dpavlin 20 $self->{'loader'} = Class::DBI::Loader->new(
126     debug => $self->{'debug'},
127 dpavlin 92 dsn => $self->{'dsn'},
128 dpavlin 20 user => $self->{'user'},
129     password => $self->{'passwd'},
130     namespace => "Nos",
131 dpavlin 59 additional_classes => qw/Class::DBI::AbstractSearch/,
132 dpavlin 20 # additional_base_classes => qw/My::Stuff/,
133     relationships => 1,
134 dpavlin 22 ) || croak "can't init Class::DBI::Loader";
135 dpavlin 20
136 dpavlin 36 $self->{'hash_len'} ||= 8;
137    
138 dpavlin 20 $self ? return $self : return undef;
139     }
140    
141 dpavlin 30
142 dpavlin 72 =head2 create_list
143 dpavlin 33
144 dpavlin 67 Create new list. Required arguments are name of C<list>, C<email> address
145     and path to C<aliases> file.
146 dpavlin 33
147 dpavlin 72 $nos->create_list(
148 dpavlin 38 list => 'My list',
149 dpavlin 47 from => 'Outgoing from comment',
150 dpavlin 33 email => 'my-list@example.com',
151 dpavlin 67 aliases => '/etc/mail/mylist',
152     archive => '/path/to/mbox/archive',
153 dpavlin 33 );
154    
155     Returns ID of newly created list.
156    
157 dpavlin 60 Calls internally C<_add_list>, see details there.
158 dpavlin 38
159 dpavlin 33 =cut
160    
161 dpavlin 72 sub create_list {
162 dpavlin 33 my $self = shift;
163    
164     my $arg = {@_};
165    
166     confess "need list name" unless ($arg->{'list'});
167 dpavlin 52 confess "need list email" unless ($arg->{'email'});
168 dpavlin 33
169 dpavlin 52 $arg->{'list'} = lc($arg->{'list'});
170     $arg->{'email'} = lc($arg->{'email'});
171    
172 dpavlin 33 my $l = $self->_get_list($arg->{'list'}) ||
173     $self->_add_list( @_ ) ||
174     return undef;
175    
176     return $l->id;
177     }
178    
179    
180 dpavlin 72 =head2 drop_list
181 dpavlin 63
182     Delete list from database.
183    
184 dpavlin 72 my $ok = drop_list(
185 dpavlin 63 list => 'My list'
186 dpavlin 70 aliases => '/etc/mail/mylist',
187 dpavlin 63 );
188    
189     Returns false if list doesn't exist.
190    
191     =cut
192    
193 dpavlin 72 sub drop_list {
194 dpavlin 63 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 dpavlin 70 my $aliases = $args->{'aliases'} || croak "need path to aliases file";
203    
204 dpavlin 63 my $lists = $self->{'loader'}->find_class('lists');
205    
206     my $this_list = $lists->search( name => $args->{'list'} )->first || return;
207    
208 dpavlin 70 $self->_remove_alias( email => $this_list->email, aliases => $aliases);
209    
210 dpavlin 63 $this_list->delete || croak "can't delete list\n";
211    
212     return $lists->dbi_commit || croak "can't commit";
213     }
214    
215    
216 dpavlin 23 =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 dpavlin 56 ext_id => 42,
225 dpavlin 23 );
226    
227 dpavlin 56 C<name> and C<ext_id> parametars are optional.
228 dpavlin 23
229 dpavlin 27 Return member ID if user is added.
230 dpavlin 23
231     =cut
232    
233     sub add_member_to_list {
234     my $self = shift;
235    
236     my $arg = {@_};
237    
238 dpavlin 52 my $email = lc($arg->{'email'}) || croak "can't add user without e-mail";
239 dpavlin 23 my $name = $arg->{'name'} || '';
240 dpavlin 52 my $list_name = lc($arg->{'list'}) || croak "need list name";
241 dpavlin 56 my $ext_id = $arg->{'ext_id'};
242 dpavlin 23
243 dpavlin 30 my $list = $self->_get_list($list_name) || croak "list $list_name doesn't exist";
244    
245 dpavlin 23 if (! Email::Valid->address($email)) {
246 dpavlin 33 carp "SKIPPING $name <$email>\n";
247 dpavlin 23 return 0;
248     }
249    
250 dpavlin 29 carp "# $name <$email>\n" if ($self->{'verbose'});
251 dpavlin 23
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 dpavlin 45 if ($name && $this_user->name ne $name) {
260     $this_user->name($name || '');
261 dpavlin 33 $this_user->update;
262     }
263    
264 dpavlin 56 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 dpavlin 23 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 dpavlin 27 return $this_user->id;
279 dpavlin 23 }
280    
281 dpavlin 43 =head2 list_members
282    
283 dpavlin 45 List all members of some list.
284    
285 dpavlin 43 my @members = list_members(
286     list => 'My list',
287     );
288    
289 dpavlin 74 Returns array of hashes with user information like this:
290 dpavlin 43
291     $member = {
292 dpavlin 45 name => 'Dobrica Pavlinusic',
293 dpavlin 43 email => 'dpavlin@rot13.org
294     }
295    
296 dpavlin 56 If list is not found, returns false. If there is C<ext_id> in user data,
297 dpavlin 60 it will also be returned.
298 dpavlin 45
299 dpavlin 43 =cut
300    
301     sub list_members {
302     my $self = shift;
303    
304     my $args = {@_};
305    
306 dpavlin 52 my $list_name = lc($args->{'list'}) || confess "need list name";
307 dpavlin 43
308     my $lists = $self->{'loader'}->find_class('lists');
309     my $user_list = $self->{'loader'}->find_class('user_list');
310    
311 dpavlin 45 my $this_list = $lists->search( name => $list_name )->first || return;
312 dpavlin 43
313     my @results;
314    
315     foreach my $user_on_list ($user_list->search(list_id => $this_list->id)) {
316     my $row = {
317 dpavlin 45 name => $user_on_list->user_id->name,
318 dpavlin 43 email => $user_on_list->user_id->email,
319     };
320    
321 dpavlin 56 my $ext_id = $user_on_list->user_id->ext_id;
322     $row->{'ext_id'} = $ext_id if (defined($ext_id));
323    
324 dpavlin 43 push @results, $row;
325     }
326    
327     return @results;
328    
329     }
330    
331    
332 dpavlin 45 =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 dpavlin 60 This function will delete member from all lists (by cascading delete), so it
347     shouldn't be used lightly.
348    
349 dpavlin 45 =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 dpavlin 52 $args->{'email'} = lc($args->{'email'}) if ($args->{'email'});
359    
360 dpavlin 45 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 dpavlin 59 =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 dpavlin 62 my $this_user_list = $user_list->search_where( list_id => $this_list->id, user_id => $this_user->id )->first || return;
406 dpavlin 59
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 dpavlin 29 =head2 add_message_to_list
413 dpavlin 24
414     Adds message to one list's queue for later sending.
415    
416 dpavlin 29 $nos->add_message_to_list(
417 dpavlin 24 list => 'My list',
418 dpavlin 36 message => 'Subject: welcome to list
419 dpavlin 38
420 dpavlin 24 This is example message
421     ',
422     );
423    
424     On success returns ID of newly created (or existing) message.
425    
426 dpavlin 36 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 dpavlin 24 =cut
431    
432 dpavlin 29 sub add_message_to_list {
433 dpavlin 24 my $self = shift;
434    
435     my $args = {@_};
436    
437 dpavlin 52 my $list_name = lc($args->{'list'}) || confess "need list name";
438 dpavlin 24 my $message_text = $args->{'message'} || croak "need message";
439    
440 dpavlin 29 my $m = Email::Simple->new($message_text) || croak "can't parse message";
441    
442 dpavlin 87 warn "message doesn't have Subject header\n" unless( $m->header('Subject') );
443 dpavlin 29
444 dpavlin 24 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 dpavlin 22 =head2 send_queued_messages
472 dpavlin 20
473 dpavlin 22 Send queued messages or just ones for selected list
474 dpavlin 20
475 dpavlin 49 $nos->send_queued_messages(
476     list => 'My list',
477     driver => 'smtp',
478     sleep => 3,
479     );
480 dpavlin 20
481 dpavlin 47 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 dpavlin 75 Any other driver name will try to use C<Email::Send::that_driver> module.
495    
496 dpavlin 49 Default sleep wait between two messages is 3 seconds.
497    
498 dpavlin 75 This method will return number of succesfully sent messages.
499    
500 dpavlin 21 =cut
501 dpavlin 20
502 dpavlin 22 sub send_queued_messages {
503 dpavlin 21 my $self = shift;
504 dpavlin 20
505 dpavlin 49 my $arg = {@_};
506 dpavlin 20
507 dpavlin 52 my $list_name = lc($arg->{'list'}) || '';
508 dpavlin 49 my $driver = $arg->{'driver'} || '';
509     my $sleep = $arg->{'sleep'};
510     $sleep ||= 3 unless defined($sleep);
511 dpavlin 47
512 dpavlin 75 # number of messages sent o.k.
513     my $ok = 0;
514    
515 dpavlin 49 my $email_send_driver = 'Email::Send::IO';
516     my @email_send_options;
517    
518 dpavlin 47 if (lc($driver) eq 'smtp') {
519     $email_send_driver = 'Email::Send::SMTP';
520     @email_send_options = ['127.0.0.1'];
521 dpavlin 75 } elsif ($driver && $driver ne '') {
522     $email_send_driver = 'Email::Send::' . $driver;
523 dpavlin 52 } else {
524     warn "dumping all messages to STDERR\n";
525 dpavlin 47 }
526    
527 dpavlin 22 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 dpavlin 20
532 dpavlin 22 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 dpavlin 20
542 dpavlin 22 while (my $m = $my_q->next) {
543     next if ($m->all_sent);
544 dpavlin 20
545 dpavlin 22 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 dpavlin 20
548 dpavlin 22 foreach my $u ($user_list->search(list_id => $m->list_id)) {
549 dpavlin 20
550 dpavlin 29 my $to_email = $u->user_id->email;
551    
552 dpavlin 32 my ($from,$domain) = split(/@/, $u->list_id->email, 2);
553    
554 dpavlin 22 if ($sent->search( message_id => $m->message_id, user_id => $u->user_id )) {
555 dpavlin 29 print "SKIP $to_email message allready sent\n";
556 dpavlin 22 } else {
557 dpavlin 65 print "=> $to_email ";
558 dpavlin 20
559 dpavlin 32 my $secret = $m->list_id->name . " " . $u->user_id->email . " " . $m->message_id;
560 dpavlin 36 my $auth = Email::Auth::AddressHash->new( $secret, $self->{'hash_len'} );
561 dpavlin 32
562 dpavlin 29 my $hash = $auth->generate_hash( $to_email );
563 dpavlin 20
564 dpavlin 47 my $from_addr;
565 dpavlin 49 my $from_email_only = $from . "+" . $hash . ( $domain ? '@' . $domain : '');
566 dpavlin 48
567 dpavlin 47 $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 dpavlin 29
571 dpavlin 32 my $m_obj = Email::Simple->new($msg) || croak "can't parse message";
572 dpavlin 29
573 dpavlin 49 $m_obj->header_set('Return-Path', $from_email_only) || croak "can't set Return-Path: header";
574 dpavlin 86 #$m_obj->header_set('Sender', $from_email_only) || croak "can't set Sender: header";
575 dpavlin 49 $m_obj->header_set('Errors-To', $from_email_only) || croak "can't set Errors-To: header";
576 dpavlin 47 $m_obj->header_set('From', $from_addr) || croak "can't set From: header";
577 dpavlin 32 $m_obj->header_set('To', $to) || croak "can't set To: header";
578 dpavlin 29
579 dpavlin 38 $m_obj->header_set('X-Nos-Version', $VERSION);
580     $m_obj->header_set('X-Nos-Hash', $hash);
581    
582 dpavlin 47 # really send e-mail
583 dpavlin 65 my $sent_status;
584    
585 dpavlin 47 if (@email_send_options) {
586 dpavlin 65 $sent_status = send $email_send_driver => $m_obj->as_string, @email_send_options;
587 dpavlin 47 } else {
588 dpavlin 65 $sent_status = send $email_send_driver => $m_obj->as_string;
589 dpavlin 47 }
590 dpavlin 22
591 dpavlin 65 croak "can't send e-mail: $sent_status\n\nOriginal e-mail follows:\n".$m_obj->as_string unless ($sent_status);
592 dpavlin 75 my @bad;
593     @bad = @{ $sent_status->prop('bad') } if (eval { $sent_status->can('prop') });
594 dpavlin 65 croak "failed sending to ",join(",",@bad) if (@bad);
595 dpavlin 49
596 dpavlin 65 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 dpavlin 75 $ok++;
608 dpavlin 65 } else {
609     warn "ERROR: $sent_status\n";
610     }
611    
612 dpavlin 49 if ($sleep) {
613     warn "sleeping $sleep seconds\n";
614     sleep($sleep);
615     }
616 dpavlin 22 }
617     }
618     $m->all_sent(1);
619     $m->update;
620     $m->dbi_commit;
621     }
622    
623 dpavlin 75 return $ok;
624    
625 dpavlin 20 }
626    
627 dpavlin 29 =head2 inbox_message
628    
629     Receive single message for list's inbox.
630    
631 dpavlin 36 my $ok = $nos->inbox_message(
632     list => 'My list',
633     message => $message,
634     );
635 dpavlin 29
636 dpavlin 60 This method is used by C<sender.pl> when receiving e-mail messages.
637    
638 dpavlin 29 =cut
639    
640     sub inbox_message {
641     my $self = shift;
642    
643 dpavlin 36 my $arg = {@_};
644 dpavlin 29
645 dpavlin 36 return unless ($arg->{'message'});
646     croak "need list name" unless ($arg->{'list'});
647 dpavlin 29
648 dpavlin 52 $arg->{'list'} = lc($arg->{'list'});
649    
650 dpavlin 37 my $this_list = $self->_get_list($arg->{'list'}) || croak "can't find list ".$arg->{'list'}."\n";
651    
652 dpavlin 36 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 dpavlin 48 my $return_path = $m->header('Return-Path') || '';
657    
658 dpavlin 36 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 dpavlin 52 if ($a->address =~ m/\+([a-f0-9]{$hl})@/i) {
668 dpavlin 36 $hash = $1;
669     last;
670     }
671     }
672    
673 dpavlin 50 #warn "can't find hash in e-mail $to\n" unless ($hash);
674 dpavlin 36
675     my $sent = $self->{'loader'}->find_class('sent');
676    
677     # will use null if no matching message_id is found
678 dpavlin 50 my $sent_msg;
679     $sent_msg = $sent->search( hash => $hash )->first if ($hash);
680 dpavlin 36
681 dpavlin 37 my ($message_id, $user_id) = (undef, undef); # init with NULL
682 dpavlin 36
683 dpavlin 37 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 dpavlin 47 } else {
687 dpavlin 50 #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 dpavlin 52 my $this_user = $users->search( email => lc($from) )->first;
692 dpavlin 50 $user_id = $this_user->id if ($this_user);
693 dpavlin 37 }
694    
695    
696     my $is_bounce = 0;
697    
698 dpavlin 49 if ($return_path eq '<>' || $return_path eq '') {
699 dpavlin 47 no warnings;
700     my $bounce = eval { Mail::DeliveryStatus::BounceParser->new(
701     $arg->{'message'}, { report_non_bounces=>1 },
702     ) };
703 dpavlin 50 #warn "can't check if this message is bounce!" if ($@);
704 dpavlin 47
705     $is_bounce++ if ($bounce && $bounce->is_bounce);
706     }
707 dpavlin 37
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 dpavlin 49 # print "message_id: ",($message_id || "not found")," -- $is_bounce\n";
721 dpavlin 29 }
722    
723 dpavlin 75 =head2 received_messages
724 dpavlin 29
725 dpavlin 75 Returns all received messages for given list or user.
726    
727 dpavlin 80 my @received = $nos->received_messages(
728 dpavlin 75 list => 'My list',
729     email => "john.doe@example.com",
730 dpavlin 80 from_date => '2005-01-01 10:15:00',
731     to_date => '2005-01-01 12:00:00',
732     message => 0,
733 dpavlin 75 );
734    
735 dpavlin 80 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 dpavlin 81 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 dpavlin 76 Each element in returned array will have following structure:
761 dpavlin 75
762 dpavlin 80 my $row = {
763 dpavlin 76 id => 42, # unique ID of received message
764 dpavlin 78 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 dpavlin 76 }
770    
771 dpavlin 80 If you specified C<message> option, this hash will also have C<message> key
772     which will contain whole received message.
773 dpavlin 76
774 dpavlin 75 =cut
775    
776     sub received_messages {
777     my $self = shift;
778    
779 dpavlin 77 my $arg = {@_} if (@_);
780 dpavlin 75
781 dpavlin 77 # croak "need list name or email" unless ($arg->{'list'} || $arg->{'email'});
782 dpavlin 75
783 dpavlin 77 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 dpavlin 80 };
790     $sql .= qq{ message,} if ($arg->{'message'});
791     $sql .= qq{
792 dpavlin 77 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 dpavlin 75
798 dpavlin 81 my $order = qq{ order by date asc };
799 dpavlin 80
800 dpavlin 77 my $where;
801 dpavlin 75
802 dpavlin 77 $where->{'lists.name'} = lc($arg->{'list'}) if ($arg->{'list'});
803     $where->{'users.email'} = lc($arg->{'email'}) if ($arg->{'email'});
804 dpavlin 80 $where->{'received.date'} = { '>=', $arg->{'date_from'} } if ($arg->{'date_from'});
805     $where->{'received.date'} = { '<=', $arg->{'date_to'} } if ($arg->{'date_to'});
806 dpavlin 77
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 dpavlin 80 my $sth = $dbh->prepare($sql . $stmt . $order);
813 dpavlin 77 $sth->execute(@bind);
814 dpavlin 76 return $sth->fetchall_hash;
815 dpavlin 75 }
816    
817    
818 dpavlin 30 =head1 INTERNAL METHODS
819    
820     Beware of dragons! You shouldn't need to call those methods directly.
821    
822 dpavlin 66
823     =head2 _add_aliases
824    
825 dpavlin 71 Add or update alias in C</etc/aliases> (or equivalent) file for selected list
826 dpavlin 66
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 dpavlin 68 foreach my $o (qw/list email aliases/) {
847     croak "need $o option" unless ($arg->{$o});
848     }
849 dpavlin 66
850 dpavlin 68 my $aliases = $arg->{'aliases'};
851     my $email = $arg->{'email'};
852     my $list = $arg->{'list'};
853 dpavlin 66
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 dpavlin 67 chmod 0777, $aliases || warn "can't change permission to 0777";
859 dpavlin 66 }
860    
861 dpavlin 71 die "FATAL: aliases file $aliases is not writable\n" unless (-w $aliases);
862    
863 dpavlin 66 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 dpavlin 82 $target .= qq#"| cd $self_path && ./sender.pl --inbox='$list'"#;
885 dpavlin 66
886 dpavlin 82 # remove hostname from email to make Postfix's postalias happy
887 dpavlin 89 $email =~ s/@.+// if (not $self->{full_hostname_in_aliases});
888 dpavlin 82
889 dpavlin 68 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 dpavlin 66 }
894    
895 dpavlin 89 # $a->write($aliases) or croak "can't save aliases $aliases ".$a->error_check;
896 dpavlin 70
897 dpavlin 66 return 1;
898     }
899    
900 dpavlin 30 =head2 _add_list
901    
902     Create new list
903    
904     my $list_obj = $nos->_add_list(
905     list => 'My list',
906 dpavlin 47 from => 'Outgoing from comment',
907 dpavlin 30 email => 'my-list@example.com',
908 dpavlin 66 aliases => '/etc/mail/mylist',
909 dpavlin 30 );
910    
911     Returns C<Class::DBI> object for created list.
912    
913 dpavlin 38 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 dpavlin 30 =cut
919    
920 dpavlin 92 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 dpavlin 30 sub _add_list {
937     my $self = shift;
938    
939     my $arg = {@_};
940    
941 dpavlin 52 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 dpavlin 66 my $aliases = lc($arg->{'aliases'}) || confess "can't add list without path to aliases file";
944    
945 dpavlin 47 my $from_addr = $arg->{'from'};
946 dpavlin 30
947 dpavlin 92 my $lists = Nos::Lists->new( handle => $self->{h} );
948 dpavlin 30
949 dpavlin 66 $self->_add_aliases(
950     list => $name,
951     email => $email,
952     aliases => $aliases,
953 dpavlin 68 ) || warn "can't add alias $email for list $name";
954 dpavlin 66
955 dpavlin 92 my $l = $self->find_or_create($lists, {
956 dpavlin 30 name => $name,
957     email => $email,
958     });
959 dpavlin 47
960 dpavlin 30 croak "can't add list $name\n" unless ($l);
961    
962 dpavlin 47 if ($from_addr && $l->from_addr ne $from_addr) {
963     $l->from_addr($from_addr);
964     $l->update;
965     }
966    
967 dpavlin 30 $l->dbi_commit;
968    
969     return $l;
970    
971     }
972    
973    
974 dpavlin 66
975 dpavlin 30 =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 dpavlin 31 my $lists = $self->{'loader'}->find_class('lists') || confess "can't find lists class";
991 dpavlin 30
992 dpavlin 52 return $lists->search({ name => lc($name) })->first;
993 dpavlin 30 }
994    
995 dpavlin 70
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 dpavlin 39 ###
1030     ### SOAP
1031     ###
1032 dpavlin 30
1033 dpavlin 39 package Nos::SOAP;
1034    
1035 dpavlin 43 use Carp;
1036    
1037 dpavlin 39 =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 dpavlin 66
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 dpavlin 75 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 dpavlin 66 =cut
1073    
1074 dpavlin 39 sub new {
1075 dpavlin 90 my $class = shift;
1076     my $self = {@_};
1077 dpavlin 66
1078     croak "need aliases parametar" unless ($self->{'aliases'});
1079    
1080 dpavlin 39 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 dpavlin 72 =head2 CreateList
1089 dpavlin 39
1090 dpavlin 72 $message_id = CreateList(
1091 dpavlin 39 list => 'My list',
1092 dpavlin 56 from => 'Name of my list',
1093 dpavlin 39 email => 'my-list@example.com'
1094     );
1095    
1096     =cut
1097    
1098 dpavlin 72 sub CreateList {
1099 dpavlin 39 my $self = shift;
1100    
1101 dpavlin 68 my $aliases = $nos->{'aliases'} || croak "need 'aliases' argument to new constructor";
1102 dpavlin 66
1103 dpavlin 39 if ($_[0] !~ m/^HASH/) {
1104 dpavlin 72 return $nos->create_list(
1105 dpavlin 56 list => $_[0], from => $_[1], email => $_[2],
1106 dpavlin 66 aliases => $aliases,
1107 dpavlin 39 );
1108     } else {
1109 dpavlin 72 return $nos->create_list( %{ shift @_ }, aliases => $aliases );
1110 dpavlin 39 }
1111     }
1112    
1113 dpavlin 43
1114 dpavlin 72 =head2 DropList
1115 dpavlin 63
1116 dpavlin 72 $ok = DropList(
1117 dpavlin 63 list => 'My list',
1118     );
1119    
1120     =cut
1121    
1122 dpavlin 72 sub DropList {
1123 dpavlin 63 my $self = shift;
1124    
1125 dpavlin 70 my $aliases = $nos->{'aliases'} || croak "need 'aliases' argument to new constructor";
1126    
1127 dpavlin 63 if ($_[0] !~ m/^HASH/) {
1128 dpavlin 72 return $nos->drop_list(
1129 dpavlin 63 list => $_[0],
1130 dpavlin 70 aliases => $aliases,
1131 dpavlin 63 );
1132     } else {
1133 dpavlin 72 return $nos->drop_list( %{ shift @_ }, aliases => $aliases );
1134 dpavlin 63 }
1135     }
1136    
1137 dpavlin 39 =head2 AddMemberToList
1138    
1139     $member_id = AddMemberToList(
1140 dpavlin 43 list => 'My list',
1141     email => 'e-mail@example.com',
1142 dpavlin 58 name => 'Full Name',
1143     ext_id => 42,
1144 dpavlin 39 );
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 dpavlin 84 list => $_[0], email => $_[1], name => $_[2], ext_id => $_[3],
1154 dpavlin 39 );
1155     } else {
1156     return $nos->add_member_to_list( %{ shift @_ } );
1157     }
1158     }
1159    
1160 dpavlin 43
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 dpavlin 62 return [ $nos->list_members( list => $list_name ) ];
1183 dpavlin 43 }
1184    
1185 dpavlin 62
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 dpavlin 39 =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 dpavlin 78 =head2 MessagesReceived
1230 dpavlin 39
1231 dpavlin 78 Return statistics about received messages.
1232 dpavlin 74
1233     my @result = MessagesReceived(
1234     list => 'My list',
1235     email => 'jdoe@example.com',
1236 dpavlin 80 from_date => '2005-01-01 10:15:00',
1237     to_date => '2005-01-01 12:00:00',
1238     message => 0,
1239 dpavlin 74 );
1240    
1241 dpavlin 80 You must specify C<list> or C<email> or any combination of those two. Other
1242     parametars are optional.
1243 dpavlin 74
1244 dpavlin 76 For format of returned array element see C<received_messages>.
1245 dpavlin 74
1246 dpavlin 78 =cut
1247    
1248     sub MessagesReceived {
1249     my $self = shift;
1250    
1251     if ($_[0] !~ m/^HASH/) {
1252 dpavlin 79 die "need at least list or email" unless (scalar @_ < 2);
1253 dpavlin 85 return \@{ $nos->received_messages(
1254 dpavlin 78 list => $_[0], email => $_[1],
1255 dpavlin 80 from_date => $_[2], to_date => $_[3],
1256     message => $_[4]
1257 dpavlin 85 ) };
1258 dpavlin 78 } else {
1259 dpavlin 79 my $arg = shift;
1260     die "need list or email argument" unless ($arg->{'list'} || $arg->{'email'});
1261 dpavlin 85 return \@{ $nos->received_messages( %{ $arg } ) };
1262 dpavlin 78 }
1263     }
1264    
1265     ###
1266    
1267 dpavlin 74 =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 dpavlin 90 =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 dpavlin 25 =head1 EXPORT
1288 dpavlin 20
1289 dpavlin 27 Nothing.
1290 dpavlin 20
1291     =head1 SEE ALSO
1292    
1293     mailman, ezmlm, sympa, L<Mail::Salsa>
1294    
1295 dpavlin 25
1296 dpavlin 20 =head1 AUTHOR
1297    
1298     Dobrica Pavlinusic, E<lt>dpavlin@rot13.orgE<gt>
1299    
1300 dpavlin 25
1301 dpavlin 20 =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 dpavlin 39
1312     1;

  ViewVC Help
Powered by ViewVC 1.1.26