1#!/usr/bin/env perl
2
3# $Id: $
4
5########################################################################
6# MAIA MAILGUARD LICENSE v.1.0
7#
8# Copyright 2004 by Robert LeBlanc <rjl@renaissoft.com>
9#                   David Morton   <mortonda@dgrmm.net>
10# All rights reserved.
11#
12# PREAMBLE
13#
14# This License is designed for users of Maia Mailguard
15# ("the Software") who wish to support the Maia Mailguard project by
16# leaving "Maia Mailguard" branding information in the HTML output
17# of the pages generated by the Software, and providing links back
18# to the Maia Mailguard home page.  Users who wish to remove this
19# branding information should contact the copyright owner to obtain
20# a Rebranding License.
21#
22# DEFINITION OF TERMS
23#
24# The "Software" refers to Maia Mailguard, including all of the
25# associated PHP, Perl, and SQL scripts, documentation files, graphic
26# icons and logo images.
27#
28# GRANT OF LICENSE
29#
30# Redistribution and use in source and binary forms, with or without
31# modification, are permitted provided that the following conditions
32# are met:
33#
34# 1. Redistributions of source code must retain the above copyright
35#    notice, this list of conditions and the following disclaimer.
36#
37# 2. Redistributions in binary form must reproduce the above copyright
38#    notice, this list of conditions and the following disclaimer in the
39#    documentation and/or other materials provided with the distribution.
40#
41# 3. The end-user documentation included with the redistribution, if
42#    any, must include the following acknowledgment:
43#
44#    "This product includes software developed by Robert LeBlanc
45#    <rjl@renaissoft.com>."
46#
47#    Alternately, this acknowledgment may appear in the software itself,
48#    if and wherever such third-party acknowledgments normally appear.
49#
50# 4. At least one of the following branding conventions must be used:
51#
52#    a. The Maia Mailguard logo appears in the page-top banner of
53#       all HTML output pages in an unmodified form, and links
54#       directly to the Maia Mailguard home page; or
55#
56#    b. The "Powered by Maia Mailguard" graphic appears in the HTML
57#       output of all gateway pages that lead to this software,
58#       linking directly to the Maia Mailguard home page; or
59#
60#    c. A separate Rebranding License is obtained from the copyright
61#       owner, exempting the Licensee from 4(a) and 4(b), subject to
62#       the additional conditions laid out in that license document.
63#
64# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDER AND CONTRIBUTORS
65# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
66# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
67# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
68# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
69# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
70# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS
71# OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
72# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
73# TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
74# USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
75########################################################################
76
77use Getopt::Long;
78use DBI;
79use Net::SMTP;
80
81# prototypes
82sub output($);
83sub fatal($);
84sub dedupe($);
85sub read_users_file($$);
86sub send_email($$$$$);
87sub resend_mail($$$$$$);
88
89# name of this script
90my $script_name = "resend";
91
92# read configuration file (/usr/local/etc/maia/maia.conf)
93my $config_file = "/usr/local/etc/maia/maia.conf";
94unless (my $rv = do $config_file) {
95    fatal(sprintf("Couldn't parse %s: %s", $config_file, $@)) if $@;
96    fatal(sprintf("Couldn't open %s", $config_file)) if (!defined($rv) || !$rv);
97};
98
99# defaults
100my @users = ();
101my $users_from_file = undef;
102my $from_date = undef;
103my $to_date = undef;
104my $from_id = undef;
105my $to_id = undef;
106my $debug = 0;
107my $help = 0;
108my $delay = 0;
109my $sysconfig_ref = {};
110
111GetOptions("users=s" => \@users,
112           "users-from-file=s" => \$users_from_file,
113           "from-date=s" => \$from_date,
114           "to-date=s" => \$to_date,
115           "from-id=i" => \$from_id,
116           "to-id=i" => \$to_id,
117           "delay=i" => \$delay,
118           "debug" => \$debug,
119           "help" => \$help);
120
121@users = split(/,/, join(',', @users));
122
123if ($help) {
124    output("resend.pl\n" .
125           "   --users addr1,addr2,...  : resend mail for selected users\n" .
126           "   --users-from-file file   : same as --users, but reads user list from a file\n" .
127           "   --users all              : same as --users, except for all users\n" .
128           "   --from-date yyyy-mm-dd   : oldest mail to resend (default: oldest in cache)\n" .
129           "   --to-date yyyy-mm-dd     : newest mail to resend (default: newest in cache)\n" .
130           "   --from-id id             : lowest mail ID to resend\n" .
131           "   --to-id id               : highest mail ID to resend\n" .
132           "   --delay seconds          : seconds to delay between emails (default: 0)\n" .
133           "   --debug                  : display detailed debugging information\n" .
134           "   --help                   : display this help text\n");
135    exit;
136}
137
138# database configuration
139if (defined($dsn) && defined($username) && defined($password)) {
140
141    # connect to the database
142    $dbh = DBI->connect($dsn, $username, $password)
143        or fatal("Can't connect to the Maia database (verify \$dsn, \$username, and \$password in maia.conf)");
144
145    # do a one-time load of some system configuration parameters
146    my $query = "SELECT smtp_server, smtp_port FROM maia_config WHERE id = 0";
147	my $sth = $dbh->prepare($query)
148	              or fatal(sprintf("Couldn't prepare query: %s", $dbh->errstr));
149	$sth->execute()
150	    or fatal(sprintf("Couldn't execute query: %s", $dbh->errstr));
151	if (my @row = $sth->fetchrow_array()) {
152	    $sysconfig_ref->{'smtp_server'} = $1 if $row[0] =~ /^(.+)$/si; # untaint
153	    $sysconfig_ref->{'smtp_port'} = $1 if $row[1] =~ /^([1-9]+[0-9]*)$/si; # untaint
154	};
155	$sth->finish;
156
157} else {
158    fatal("Can't connect to the Maia database (missing \$dsn, \$username, or \$password in maia.conf)");
159}
160
161
162# Read the (optional) input files for user addresses
163read_users_file(\@users, $users_from_file)
164    if (defined($users_from_file));
165
166# Remove any duplicates
167dedupe(\@users);
168
169# Sanity-check user-supplied arguments
170if (!@users) {
171    fatal("No users specified.");
172}
173if (defined($from_date)) {
174    if ($from_date =~ /^(\d{4})[\-\/:](\d{2})[\-\/:](\d{2})$/) {
175        my $year = $1;
176        my $month = $2;
177        my $day = $3;
178        my $yearnow = (localtime)[5] + 1900;
179        if ($year < 1900 || $year > $yearnow || $month < 1 || $month > 12 || $day < 1 || $day > 31) {
180            fatal("'From' date parameters out of range, use YYYY-MM-DD.");
181        }
182    } else {
183        fatal("Invalid 'From' date, use YYYY-MM-DD.");
184    }
185}
186if (defined($to_date)) {
187    if ($to_date =~ /^(\d{4})[\-\/:](\d{2})[\-\/:](\d{2})$/) {
188        my $year = $1;
189        my $month = $2;
190        my $day = $3;
191        my $yearnow = (localtime)[5] + 1900;
192        if ($year < 1900 || $year > $yearnow || $month < 1 || $month > 12 || $day < 1 || $day > 31) {
193            fatal("'To' date parameters out of range, use YYYY-MM-DD.");
194        }
195    } else {
196        fatal("Invalid 'To' date, use YYYY-MM-DD.");
197    }
198}
199if (defined($from_id)) {
200    if ($from_id < 1) {
201        fatal("Invalid mail ID, must be a positive integer.");
202    }
203}
204if (defined($to_id)) {
205    if ($to_id < 1) {
206        fatal("Invalid mail ID, must be a positive integer.");
207    }
208}
209
210
211# Resend cached email
212foreach my $user (@users) {
213    resend_mail($dbh, $user, $from_date, $to_date, $from_id, $to_id);
214}
215
216# Disconnect from the database
217$dbh->disconnect;
218
219exit;
220
221########################################################################
222###              End of Script: Subroutines begin below              ###
223########################################################################
224
225# Die, printing a time-stamped error message.
226sub fatal($) {
227    my ($msg) = @_;
228
229    output("FATAL ERROR: " . $msg);
230    exit 1;
231}
232
233
234# Write a time-stamped string to stdout for logging purposes.
235sub output($) {
236    my ($msg) = @_;
237    my ($second, $minute, $hour, $day, $month, $year) = (localtime)[0,1,2,3,4,5];
238
239    printf("%04d-%02d-%02d %02d:%02d:%02d Maia: [%s] %s\n",
240           $year+1900, $month+1, $day, $hour, $minute, $second, $script_name, $msg);
241}
242
243
244# Remove any duplicate array elements
245sub dedupe($) {
246    my($array_ref) = @_;
247
248    my %seen = ();
249    foreach my $element (@$array_ref) {
250        $seen{$element}++;
251    }
252    @$array_ref = keys %seen;
253}
254
255
256# Sends an e-mail via the downstream MTA
257sub send_email($$$$$) {
258    my ($smtp_server, $smtp_port, $from, $to, $body) = @_;
259
260    my($smtp) = Net::SMTP->new($smtp_server, Port => $smtp_port) ;
261    fatal("Couldn't connect to SMTP server: $!") unless $smtp;
262    $smtp->mail($from) ;
263    $smtp->to($to) ;
264    $smtp->data() ;
265    $smtp->datasend($body . "\n") ;
266    $smtp->dataend() ;
267    $smtp->quit() ;
268}
269
270
271# Parse an input file for lines containing e-mail addresses.  Blank lines
272# and whitespace should be ignored.
273sub read_users_file($$) {
274    my($addresses_ref, $infile) = @_;
275
276    open (INFILE, "< $infile")
277        or fatal("Can't read input file $infile: $!");
278    while (<INFILE>) {
279        chomp;
280        if ($_ =~ /^\s*(.+@.+)\s*$/) {
281            push(@$addresses_ref, $1);
282        } elsif ($_ =~ /^all$/i) {
283            push(@$addresses_ref, '*');
284        } else {
285	        # skip blank lines
286        }
287    }
288    close INFILE;
289}
290
291
292sub resend_mail($$$$$$) {
293    my($dbh, $user, $from_date, $to_date, $from_id, $to_id) = @_;
294    my $user_clause = '';
295    my $from_clause = '';
296    my $to_clause = '';
297    my $fromid_clause = '';
298    my $toid_clause = '';
299    my @params = ();
300
301    if ($user ne '*') {
302        $user_clause = " AND users.email = ?";
303        push @params, $user;
304        output("Resending cached emails for $user") if ($debug);
305    } else {
306        output("Resending cached emails for all users") if ($debug);
307    }
308
309    if (defined($from_date)) {
310        $from_date .= ' 00:00:00';
311        $from_clause = " AND maia_mail.received_date >= ?";
312        push @params, $from_date;
313        output("Items received no earlier than $from_date") if ($debug);
314    }
315
316    if (defined($to_date)) {
317        $to_date .= ' 23:59:59';
318        $to_clause = " AND maia_mail.received_date <= ?";
319        push @params, $to_date;
320        output("Items received no later than $to_date") if ($debug);
321    }
322
323    if (defined($from_id)) {
324        $fromid_clause = " AND maia_mail.id >= ?";
325        push @params, $from_id;
326        output("Item ID no lower than $from_id") if ($debug);
327    }
328
329    if (defined($to_id)) {
330        $toid_clause = " AND maia_mail.id <= ?";
331        push @params, $to_id;
332        output("Item ID no higher than $to_id") if ($debug);
333    }
334
335    if ($delay && $debug) {
336        output("Pausing for $delay seconds between mailings");
337    }
338
339    my $query = <<EOQ;
340SELECT maia_mail.id, email, sender_email, received_date, contents
341FROM maia_mail, maia_mail_recipients, maia_users, users
342WHERE maia_mail.id = maia_mail_recipients.mail_id
343AND maia_mail_recipients.recipient_id = maia_users.id
344AND maia_users.primary_email_id = users.id
345AND maia_mail_recipients.type = 'H'
346EOQ
347
348    $query .= $user_clause . $from_clause . $to_clause . $fromid_clause . $toid_clause;
349    $query .= " ORDER BY received_date ASC";
350
351    my $sth = $dbh->prepare($query)
352        or fatal(sprintf("Couldn't prepare query: %s", $dbh->errstr));
353
354    $sth->execute(@params)
355        or fatal(sprintf("Couldn't execute query: %s", $dbh->errstr));
356
357    my $resent_count = 0;
358    while (my @row = $sth->fetchrow()) {
359        my $mail_id = $row[0];
360        my $mail_recipient = $row[1];
361        my $mail_sender = $row[2];
362        my $mail_date = $row[3];
363        my $mail_contents = $row[4];
364
365        if ($debug) {
366            output("Mail ID $mail_id: Received $mail_date : From $mail_sender : To $mail_recipient");
367        }
368
369        send_email($sysconfig_ref->{'smtp_server'},
370                   $sysconfig_ref->{'smtp_port'},
371                   $mail_sender,
372                   $mail_recipient,
373                   $mail_contents);
374
375        $resent_count++;
376
377        if ($delay) {
378            sleep($delay);
379        }
380    }
381    $sth->finish;
382
383    output("$resent_count emails redelivered.");
384
385}
386
387