babf46b2f819adeb6a844d9eb7101fdd91a7629e
[clearscm.git] / maps / bin / MAPS.pm
1 #!/usr/bin/perl
2 #################################################################################
3 #
4 # File:         $RCSfile: MAPS.pm,v $
5 # Revision:  $Revision: 1.1 $
6 # Description:  Main module for Mail Authentication and Permission System (MAPS)
7 # Author:       Andrew@DeFaria.com
8 # Created:      Fri Nov 29 14:17:21  2002
9 # Modified:     $Date: 2013/06/12 14:05:47 $
10 # Language:     perl
11 #
12 # (c) Copyright 2000-2006, Andrew@DeFaria.com, all rights reserved.
13 #
14 ################################################################################
15 package MAPS;
16
17 use strict;
18
19 use FindBin;
20
21 use MAPSDB;
22 use MAPSLog;
23 use MAPSFile;
24 use MAPSUtil;
25 use MIME::Entity;
26
27 use vars qw (@ISA @EXPORT);
28 use Exporter;
29
30 @ISA = qw (Exporter);
31
32 @EXPORT = qw (
33   Add2Blacklist
34   Add2Nulllist
35   Add2Whitelist
36   AddEmail
37   AddList
38   AddUser
39   AddUserOptions
40   Blacklist
41   CleanEmail
42   CleanLog
43   CleanList
44   CountMsg
45   Decrypt
46   DeleteEmail
47   DeleteList
48   DeleteLog
49   Encrypt
50   FindEmail
51   FindList
52   FindLog
53   FindUser
54   ForwardMsg
55   GetContext
56   GetEmail
57   GetList
58   GetLog
59   GetUser
60   GetUserOptions
61   ListLog
62   ListUsers
63   Login
64   Nulllist
65   OnBlacklist
66   OnNulllist
67   OnWhitelist
68   OptimizeDB
69   ReadMsg
70   ResequenceList
71   ReturnList
72   ReturnListEntry
73   ReturnMsg
74   ReturnMessages
75   ReturnSenders
76   SaveMsg
77   SearchEmails
78   SetContext
79   Space
80   UpdateList
81   UpdateUser
82   UpdateUserOptions
83   UserExists
84   Whitelist
85 );
86
87 my $mapsbase = "$FindBin::Bin/..";
88
89 # Forwards
90 sub Add2Blacklist;
91 sub Add2Nulllist;
92 sub Add2Whitelist;
93 sub AddEmail;
94 sub AddList;
95 sub AddUser;
96 sub AddUserOptions;
97 sub Blacklist;
98 sub CleanEmail;
99 sub CleanLog;
100 sub CountMsg;
101 sub Decrypt;
102 sub DeleteEmail;
103 sub DeleteList;
104 sub DeleteLog;
105 sub Encrypt;
106 sub FindEmail;
107 sub FindList;
108 sub FindLog;
109 sub FindUser;
110 sub ForwardMsg;
111 sub GetContext;
112 sub GetEmail;
113 sub GetList;
114 sub GetLog;
115 sub GetUser;
116 sub GetUserOptions;
117 sub Login;
118 sub Nulllist;
119 sub OnBlacklist;
120 sub OnNulllist;
121 sub OnWhitelist;
122 sub OptimizeDB;
123 sub ReadMsg;
124 sub ResequenceList;
125 sub ReturnList;
126 sub ReturnListEntry;
127 sub ReturnMsg;
128 sub ReturnMessages;
129 sub ReturnSenders;
130 sub SaveMsg;
131 sub SearchEmails;
132 sub SendMsg;
133 sub SetContext;
134 sub Space;
135 sub UpdateList;
136 sub UpdateUser;
137 sub UpdateUserOptions;
138 sub UserExists;
139 sub Whitelist;
140
141 BEGIN {
142   my $MAPS_username = "maps";
143   my $MAPS_password = "spam";
144
145   OpenDB $MAPS_username, $MAPS_password;
146 } # BEGIN
147
148 END {
149   CloseDB;
150 } # END
151
152 sub Add2Blacklist {
153   # Add2Blacklist will add an entry to the blacklist
154   my ($sender, $userid, $comment) = @_;
155
156   # First SetContext to the userid whose black list we are adding to
157   MAPSDB::SetContext $userid;
158
159   # Add to black list
160   AddList "black", $sender, 0, $comment;
161
162   # Log that we black listed the sender
163   Info "Added $sender to " . ucfirst $userid . "'s black list";
164
165   # Delete old emails
166   my $count = DeleteEmail $sender;
167
168   # Log out many emails we managed to remove
169   Info "Removed $count emails from $sender"
170 } # Add2Blacklist
171
172 sub Add2Nulllist ($$;$$) {
173   # Add2Nulllist will add an entry to the nulllist
174   my ($sender, $userid, $comment, $hit_count) = @_;
175   
176   # First SetContext to the userid whose null list we are adding to
177   MAPSDB::SetContext $userid;
178
179   # Add to null list
180   AddList "null", $sender, 0, $comment, $hit_count;
181
182   # Log that we null listed the sender
183   Info "Added $sender to " . ucfirst $userid . "'s null list";
184
185   # Delete old emails
186   my $count = DeleteEmail $sender;
187
188   # Log out many emails we managed to remove
189   Info "Removed $count emails from $sender"
190 } # Add2Nulllist
191
192 sub Add2Whitelist ($$;$) {
193   # Add2Whitelist will add an entry to the whitelist
194   my ($sender, $userid, $comment) = @_;
195
196   # First SetContext to the userid whose white list we are adding to
197   MAPSDB::SetContext $userid;
198
199   # Add to white list
200   AddList 'white', $sender, 0, $comment;
201
202   # Log that we registered a user
203   Logmsg "registered", $sender, "Registered new sender";
204
205   # Check to see if there are any old messages to deliver
206   my $handle = FindEmail $sender;
207
208   my ($dbsender, $subject, $timestamp, $message);
209
210   # Deliver old emails
211   my $messages    = 0;
212   my $return_status  = 0;
213
214   while (($userid, $dbsender, $subject, $timestamp, $message) = GetEmail $handle) {
215     last 
216       unless $userid;
217
218     $return_status = Whitelist $sender, $message;
219
220     last
221       if $return_status;
222
223     $messages++;
224   } # while
225
226   # Done with $handle
227   $handle->finish;
228
229   # Return if we has a problem delivering email
230   return $return_status
231     if $return_status;
232
233   # Remove delivered messages.
234   DeleteEmail $sender;
235
236   return $messages;
237 } # Add2Whitelist
238
239 sub AddEmail ($$$) {
240   my ($sender, $subject, $data) = @_;
241
242   MAPSDB::AddEmail $sender, $subject, $data;
243 } # AddEmail
244
245 sub AddList ($$$;$$$) {
246   my ($listtype, $pattern, $sequence, $comment, $hit_count, $last_hit) = @_;
247
248   $hit_count //= CountMsg $pattern;
249
250   MAPSDB::AddList $listtype, $pattern, $sequence, $comment, $hit_count, $last_hit;
251 } # AddList
252
253 sub AddUser ($$$$) {
254   my ($userid, $realname, $email, $password) = @_;
255
256   return MAPSDB::AddUser $userid, $realname, $email, $password;
257 } # AddUser
258
259 sub AddUserOptions ($%) {
260   my ($userid, %options) = @_;
261
262   my $status;
263
264   foreach (keys (%options)) {
265     $status = MAPSDB::AddUserOption $userid, $_, $options{$_};
266     last if $status ne 0;
267   } # foreach
268
269   return $status;
270 } # AddUserOptions
271
272 sub Blacklist ($$$@) {
273   # Blacklist will send a message back to the $sender telling them that
274   # they've been blacklisted. Currently we save a copy of the message.
275   # In the future we should just disregard the message.
276   my ($sender, $sequence, $hit_count, @msg)  = @_;
277
278   # Check to see if this sender has already emailed us.
279   my $msg_count = CountMsg $sender;
280
281   if ($msg_count lt 5) {
282     # Bounce email
283     SendMsg ($sender, "Your email has been discarded by MAPS", "$mapsbase/blacklist.html", @msg);
284     Logmsg "blacklist", $sender, "Sent blacklist reply";
285   } else {
286     Logmsg "mailloop", $sender, "Mail loop encountered";
287   } # if
288
289   RecordHit "black", $sequence, ++$hit_count if $sequence;
290 } # Blacklist
291
292 sub CleanEmail ($) {
293   my ($timestamp) = @_;
294
295   MAPSDB::CleanEmail $timestamp;
296 } # CleanEmail
297
298 sub CleanLog ($) {
299   my ($timestamp) = @_;
300
301   MAPSDB::CleanLog $timestamp;
302 } # CleanLog
303
304 sub CleanList ($;$) {
305   my ($timestamp, $listtype) = @_;
306
307   MAPSDB::CleanList $timestamp, $listtype;
308 } # CleanList
309
310 sub CountMsg ($) {
311   my ($sender) = @_;
312
313   return MAPSDB::CountMsg $sender;
314 } # CountMsg
315
316 sub Decrypt ($$) {
317   my ($password, $userid) = @_;
318
319   return MAPSDB::Decrypt $password, shift;
320 } # Decrypt
321
322 sub DeleteEmail ($) {
323   my ($sender) = @_;
324
325   return MAPSDB::DeleteEmail $sender;
326 } # DeleteEmail
327
328 sub DeleteList ($$) {
329   my ($type, $sequence) = @_;
330
331   return MAPSDB::DeleteList $type, $sequence;
332 } # DeleteList
333
334 sub DeleteLog ($) {
335   my ($sender) = @_;
336
337   return MAPSDB::DeleteLog $sender;
338 } # DeleteLog
339
340 sub Encrypt ($$) {
341   my ($password, $userid) = @_;
342
343   return MAPSDB::Encrypt $password, $userid;
344 } # Encrypt
345
346 sub FindEmail (;$) {
347   my ($sender) = @_;
348
349   return MAPSDB::FindEmail $sender;
350 } # FindEmail
351
352 sub FindList ($;$) {
353   my ($type, $sender) = @_;
354
355   return MAPSDB::FindList $type, $sender;
356 } # FindList
357
358 sub FindLog ($) {
359   my ($how_many) = @_;
360
361   my $start_at = 0;
362   my $end_at   = MAPSDB::countlog ();
363
364   if ($how_many < 0) {
365     $start_at = $end_at - abs ($how_many);
366     $start_at = 0 if ($start_at < 0);
367   } # if
368
369   return MAPSDB::FindLog $start_at, $end_at;
370 } # FindLog
371
372 sub FindUser (;$) {
373   my ($userid) = @_;
374
375   return MAPSDB::FindUser $userid
376 } # FindUser
377
378 sub GetContext () {
379   return MAPSDB::GetContext ();
380 } # GetContext
381
382 sub GetEmail ($) {
383   my ($handle) = @_;
384
385   return MAPSDB::GetEmail $handle;
386 } # GetEmail
387
388 sub GetList ($) {
389   my ($handle) = @_;
390
391   return MAPSDB::GetList $handle;
392 } # GetList
393
394 sub GetLog ($) {
395   my ($handle) = @_;
396
397   return MAPSDB::GetLog $handle;
398 } # GetLog
399
400 sub GetUser ($) {
401   my ($handle) = @_;
402
403   return MAPSDB::GetUser $handle;
404 } # GetUser
405
406 sub GetUserOptions ($) {
407   my ($userid) = @_;
408
409   return MAPSDB::GetUserOptions $userid;
410 } # GetUserOptions
411
412 sub Login ($$) {
413   my ($userid, $password) = @_;
414
415   $password = Encrypt $password, $userid;
416
417   # Check if user exists
418   my $dbpassword = UserExists $userid;
419
420   # Return -1 if user doesn't exist
421   return -1 if !$dbpassword;
422
423   # Return -2 if password does not match
424   if ($password eq $dbpassword) {
425     MAPSDB::SetContext $userid;
426     return 0
427   } else {
428     return -2
429   } # if
430 } # Login
431
432 sub Nulllist ($;$$) {
433   # Nulllist will simply discard the message.
434   my ($sender, $sequence, $hit_count) = @_;
435
436   RecordHit "null", $sequence, ++$hit_count if $sequence;
437
438   # Discard Message
439   Logmsg "nulllist", $sender, "Discarded message";
440 } # Nulllist
441
442 sub OnBlacklist ($;$) {
443   my ($sender, $update) = @_;
444
445   return CheckOnList "black", $sender, $update;
446 } # CheckOnBlacklist
447
448 sub OnNulllist ($;$) {
449   my ($sender, $update) = @_;
450
451   return CheckOnList "null", $sender, $update;
452 } # CheckOnNulllist
453
454 sub OnWhitelist ($;$$) {
455   my ($sender, $userid, $update) = @_;
456
457   if (defined $userid) {
458     MAPSDB::SetContext $userid;
459   } # if
460
461   return CheckOnList "white", $sender, $update;
462 } # OnWhitelist
463
464 sub OptimizeDB () {
465   return MAPSDB::OptimizeDB ();
466 } # OptimizeDB
467
468 sub ReadMsg ($) {
469   # Reads an email message file from $input. Returns sender, subject,
470   # date and data, which is a copy of the entire message.
471   my ($input) = @_;
472
473   my $sender           = "";
474   my $sender_long      = "";
475   my $envelope_sender  = "";
476   my $reply_to         = "";
477   my $subject          = "";
478   my $data             = "";
479   my @data;
480
481   # Find first message's "From " line indicating start of message
482   while (<$input>) {
483     chomp;
484     last if /^From /;
485   } # while
486
487   # If we hit eof here then the message was garbled. Return indication of this
488   if (eof $input) {
489     $data = "Garbled message - unable to find From line";
490     return $sender, $sender_long, $reply_to, $subject, $data;
491   } # if
492
493   if (/From (\S*)/) {
494     $envelope_sender = $1;
495     $sender_long     = $envelope_sender;
496   } # if
497
498   push @data, $_ if /^From /;
499
500   while (<$input>) {
501     chomp;
502     push @data, $_;
503
504     # Blank line indicates start of message body
505     last if ($_ eq "" || $_ eq "\r");
506
507     # Extract sender's address
508     if (/^from: .*/i) {
509       $_ = substr ($_, 6);
510       
511       $sender_long = $_;
512       
513       if (/<(\S*)@(\S*)>/) {
514         $sender = lc ("$1\@$2");
515       } elsif (/(\S*)@(\S*)\ /) {
516         $sender = lc ("$1\@$2");
517       } elsif (/(\S*)@(\S*)/) {
518         $sender = lc ("$1\@$2");
519       } # if
520     } elsif (/^subject: .*/i) {
521       $subject = substr ($_, 9);
522     } elsif (/^reply-to: .*/i) {
523       $_ = substr ($_, 10);
524       if (/<(\S*)@(\S*)>/) {
525         $reply_to = lc ("$1\@$2");
526       } elsif (/(\S*)@(\S*)\ /) {
527         $reply_to = lc ("$1\@$2");
528       } elsif (/(\S*)@(\S*)/) {
529         $reply_to = lc ("$1\@$2");
530       } # if
531     } # if
532   } # while
533
534   # Read message body
535   while (<$input>) {
536     chomp;
537
538     last if (/^From /);
539     push @data, $_;
540   } # while
541
542   # Set file pointer back by length of the line just read
543   seek ($input, -length () - 1, 1) if !eof $input;
544
545   # Sanitize email addresses
546   $envelope_sender =~ s/\<//g;
547   $envelope_sender =~ s/\>//g;
548   $envelope_sender =~ s/\"//g;
549   $envelope_sender =~ s/\'//g;
550   $sender          =~ s/\<//g;
551   $sender          =~ s/\>//g;
552   $sender          =~ s/\"//g;
553   $sender          =~ s/\'//g;
554   $reply_to        =~ s/\<//g;
555   $reply_to        =~ s/\>//g;
556   $reply_to        =~ s/\"//g;
557   $reply_to        =~ s/\'//g;
558
559   # Determine best addresses
560   $sender    = $envelope_sender if $sender eq "";
561   $reply_to  = $sender          if $reply_to eq "";
562
563   return $sender, $sender_long, $reply_to, $subject, join "\n", @data;
564 } # ReadMsg
565
566 sub ResequenceList ($$) {
567   my ($userid, $type) = @_;
568
569   return MAPSDB::ResequenceList $userid, $type;
570 } # ResequenceList
571
572 sub ReturnMessages ($$) {
573   my ($userid, $sender) = @_;
574
575   return MAPSDB::ReturnMessages $userid, $sender;
576 } # ReturnMessages
577
578 sub ReturnSenders ($$$;$$) {
579   my ($userid, $type, $next, $lines, $date) = @_;
580
581   return MAPSDB::ReturnSenders $userid, $type, $next, $lines, $date;
582 } # ReturnSenders
583
584 sub ReturnList ($$$) {
585   my ($type, $start_at, $lines)  = @_;
586
587   return MAPSDB::ReturnList $type, $start_at, $lines;
588 } # ReturnList
589
590 sub ReturnListEntry ($$) {
591   my ($type, $sequence) = @_;
592
593   return MAPSDB::ReturnListEntry $type, $sequence;
594 } # ReturnList
595
596 # Added reply_to. Previously we passed reply_to into here as sender. This
597 # caused a problem in that we were filtering as per sender but logging it
598 # as reply_to. We only need reply_to for SendMsg so as to honor reply_to
599 # so we now pass in both sender and reply_to
600 sub ReturnMsg ($$$$) {
601   # ReturnMsg will send back to the $sender the register message.
602   # Messages are saved to be delivered when the $sender registers.
603   my ($sender, $reply_to, $subject, $data) = @_;
604
605   # Check to see if this sender has already emailed us.
606   my $msg_count = CountMsg $sender;
607
608   if ($msg_count < 5) {
609     # Return register message
610     my @msg;
611     foreach (split /\n/,$data) {
612       push @msg, "$_\n";
613     } # foreach
614     SendMsg $reply_to,
615             "Your email has been returned by MAPS",
616             "$mapsbase/register.html",
617             GetContext,
618             @msg
619       if $msg_count eq 0;
620     Logmsg "returned", $sender, "Sent register reply";
621     # Save message
622     SaveMsg $sender, $subject, $data;
623   } else {
624     Add2Nulllist $sender, GetContext, "Auto Null List - Mail loop";
625     Logmsg "mailloop", $sender, "Mail loop encountered";
626   } # if
627 } # ReturnMsg
628
629 sub SaveMsg ($$$) {
630   my ($sender, $subject, $data) = @_;
631
632   AddEmail $sender, $subject, $data;
633 } # SaveMsg
634
635 sub SearchEmails ($$) {
636   my ($userid, $searchfield) = @_;
637
638   return MAPSDB::SearchEmails $userid, $searchfield;
639 } # SearchEmails
640
641 sub ForwardMsg ($$$) {
642   my ($sender, $subject, $data)  = @_;
643
644   my @lines = split /\n/, $data;
645
646   while ($_ = shift @lines) {
647     last if ($_ eq "" || $_ eq "\r");
648   } # while
649
650   my $to = "renn.leech\@compassbank.com";
651
652   my $msg = MIME::Entity->build (
653     From  => $sender,
654     To    => $to,
655     Subject  => $subject,
656     Type  => "text/html",
657     Data  => \@lines,
658   );
659
660   # Send it
661   open MAIL, "| /usr/lib/sendmail -t -oi -oem"
662     or die "ForwardMsg: Unable to open pipe to sendmail $!";
663   $msg->print(\*MAIL);
664   close MAIL;
665 } # ForwardMsg
666
667 sub SendMsg ($$$$@) {
668   # SendMsg will send the message contained in $msgfile.
669   my ($sender, $subject, $msgfile, $userid, @spammsg) = @_;
670
671   my @lines;
672
673   # Open return message template file
674   open RETURN_MSG_FILE, "$msgfile"
675     or die "Unable to open return msg file ($msgfile): $!\n";
676
677   # Read return message template file and print it to $msg_body
678   while (<RETURN_MSG_FILE>) {
679     if (/\$userid/) {
680       # Replace userid
681       s/\$userid/$userid/;
682     } # if
683     if (/\$sender/) {
684       # Replace sender
685       s/\$sender/$sender/;
686     } #if
687     push @lines, $_;
688   } # while
689
690   # Close RETURN_MSG_FILE
691   close RETURN_MSG_FILE;
692
693   # Create the message, and set up the mail headers:
694   my $msg = MIME::Entity->build (
695     From  => "MAPS\@DeFaria.com",
696     To    => $sender,
697     Subject  => $subject,
698     Type  => "text/html",
699     Data  => \@lines
700   );
701
702   # Need to obtain the spam message here...
703   $msg->attach (
704     Type  => "message",
705     Disposition  => "attachment",
706     Data  => \@spammsg
707   );
708
709   # Send it
710   open MAIL, "| /usr/lib/sendmail -t -oi -oem"
711     or die "SendMsg: Unable to open pipe to sendmail $!";
712   $msg->print(\*MAIL);
713   close MAIL;
714 } # SendMsg
715
716 sub SetContext ($) {
717   my ($new_user) = @_;
718
719   return MAPSDB::SetContext $new_user;
720 } # SetContext
721
722 sub Space ($) {
723   my ($userid) = @_;
724
725   return MAPSDB::Space $userid;
726 } # Space
727
728 sub UpdateList ($$$$$$$) {
729   my ($userid, $type, $pattern, $domain, $comment, $hit_count, $sequence) = @_;
730
731   return MAPSDB::UpdateList $userid, $type, $pattern, $domain, $comment, $hit_count, $sequence;
732 } # UpdateList
733
734 sub UpdateUser ($$$$) {
735   my ($userid, $fullname, $email, $password) = @_;
736
737   return MAPSDB::UpdateUser $userid, $fullname, $email, $password;
738 } # UpdateUser
739
740 sub UpdateUserOptions ($@) {
741   my ($userid, %options)  = @_;
742
743   my $status;
744
745   foreach (keys (%options)) {
746     $status = MAPSDB::UpdateUserOption $userid, $_, $options{$_};
747     last if $status ne 0;
748   }
749
750   return $status;
751 } # UpdateUserOptions
752
753 sub UserExists ($) {
754   my ($userid) = @_;
755
756   return MAPSDB::UserExists $userid
757 } # UserExists
758
759 sub Whitelist ($$;$$) {
760   # Whitelist will deliver the message.
761   my ($sender, $data, $sequence, $hit_count) = @_;
762
763   my $userid = GetContext;
764
765   # Dump message into a file
766   open MESSAGE, ">/tmp/MAPSMessage.$$"
767     or Error "Unable to open message file (/tmp/MAPSMessage.$$): $!\n", return -1;
768
769   print MESSAGE $data;
770
771   close MESSAGE;
772
773   # Now call MAPSDeliver
774   my $status = system "$FindBin::Bin/MAPSDeliver $userid /tmp/MAPSMessage.$$";
775
776   unlink "/tmp/MAPSMessage.$$";
777
778   if ($status eq 0) {
779     Logmsg "whitelist", $sender, "Delivered message";
780   } else { 
781     Error "Unable to deliver message - is MAPSDeliver setgid? - $!";
782   } # if
783
784   RecordHit "white", $sequence, ++$hit_count if $sequence;
785
786   return $status;
787 } # Whitelist
788
789 1;