#!/usr/bin/perl $ID = q$Id: mjinject,v 1.1 2002/06/29 01:45:48 eagle Exp $; # # mjinject -- Inject a message from Majordomo into the qmail queue. # # Copyright 1997, 1998, 2002 by Russ Allbery # with help from Giles Lean # # This program is free software; you may redistribute it and/or modify it # under the same terms as Perl itself. ############################################################################## # Site configuration ############################################################################## # The name of the local host, used to qualify addresses. $hostname = 'example.com'; # VERP is on by default; to turn off, change 1 to 0. $verp = 1; # The user-ext character (- by default). This is the same as conf-break in # the qmail configuration. $userext = '-'; # The full path to programs we'll need. $qmailinject = '/var/qmail/bin/qmail-inject'; $qmailqueue = '/var/qmail/bin/qmail-queue'; # What directory to use for storing temporary files. $tempdir = $ENV{TMPDIR} || '/tmp'; ############################################################################## # Modules and declarations ############################################################################## # We need sysseek(), which wasn't added to core until 5.004. require 5.004; use strict; use vars qw($ID $hostname $qmailinject $qmailqueue $tempdir $userext $verp); use Fcntl qw(O_CREAT O_EXCL O_RDWR); ############################################################################## # Error handling ############################################################################## # Fail with the given message and a permanent failure code. sub bounce { warn @_, "\n"; exit 100 } # Fail with the given message and a temporary failure code. sub tempfail { warn @_, "\n"; exit 111 } ############################################################################## # qmail-inject handling ############################################################################## # Takes an incoming message on stdin and passes it through qmail-inject, # writing it to the given file handle. Also takes in the address to create a # Delivered-To line for. sub inject { my ($fh, $address) = @_; # We already have the message on stdin, so we don't have to change # anything there to feed it to qmail-inject. We do want to parse the # output of qmail-inject since we need to strip off the Return-Path header # that it's going to add. local $SIG{PIPE} = 'IGNORE'; my $pid = open (INJECT, '-|'); if (not defined $pid) { tempfail 'Unable to fork. (#4.3.0)'; } elsif ($pid == 0) { # In child. Exec qmail-inject and let it read the incoming message. exec $qmailinject, '-n' or tempfail 'Unable to exec qmail-inject. (#4.3.0)'; } else { # In the parent. Write out a Delivered-To header and then parse the # output of qmail-inject, stripping any Return-Path header. syswrite ($fh, "Delivered-To: $address\n", 15 + length $address) or tempfail 'Unable to write to temporary file. (#4.3.0)'; local $_; my $inheader = 1; while () { next if ($inheader && /^Return-Path:/); $inheader = 0 if /^$/; syswrite ($fh, $_, length $_) or tempfail 'Unable to write to temporary file. (#4.3.0)'; } close INJECT; if ($? != 0) { warn 'Unable to canonify message. (#4.3.0)'; exit ($? >> 8); } sysseek ($fh, 0, 0) or tempfail 'Unable to rewind temporary file. (#4.3.0)'; } } ############################################################################## # qmail-queue handling ############################################################################## # Takes in an open file handle for the message, the envelope sender (as a # string), and the name of the file that contains the list of addresses to # send the message to. sub queue { my ($message, $sender, $addresses) = @_; # Create a pipe through which to send the envelope addresses. pipe (EOUT, EIN) or tempfail 'Unable to create a pipe. (#4.3.0)'; # Fork qmail-queue. The qmail-queue child will then open fd 0 as $message # and fd 1 as the reading end of the envelope pipe and exec qmail-queue. # The parent will read in the addresses from the file one at a time and # pass them through the pipe and then check the exit status. local $SIG{PIPE} = 'IGNORE'; my $pid = fork; if (not defined $pid) { tempfail 'Unable to fork. (#4.3.0)'; } elsif ($pid == 0) { # In child. Mutilate our file handles. close EIN; open (STDIN, "<&$$message") or tempfail 'Unable to reopen fd 0. (#4.3.0)'; open (STDOUT, "<&EOUT") or tempfail 'Unable to reopen fd 1. (#4.3.0)'; exec $qmailqueue or tempfail 'Unable to fork qmail-queue. (#4.3.0)'; } else { # In parent. close EOUT; my ($user, $host); # Figure out the envelope sender and do the VERP magic if desired. if ($sender =~ /\@/) { ($user, $host) = split (/\@/, $sender); } else { $user = $sender; $host = $hostname; } if ($verp) { $user .= $userext unless $user =~ /$userext$/o } $sender = "$user\@$host"; $sender .= "$userext\@[]" if $verp; # Feed the envelope addresses to qmail-queue. print EIN "F$sender\0"; open (ADDRESSES, $addresses) or tempfail 'Unable to open address file. (#4.3.0)'; local $_; while () { my ($address) = split; $address .= "\@$hostname" if (!/\@/); print EIN "T$address\0"; } close ADDRESSES; print EIN "\0"; close EIN or tempfail 'Write error to envelope pipe. (#4.3.0)'; } # We should now have queued the message. Let's find out the exit status # of qmail-queue. waitpid ($pid, 0); if ($? != 0) { tempfail 'Unable to queue message. (#4.3.0)' } } ############################################################################## # Program invocation ############################################################################## # Given the message on a file handle followed by the command to run (as a # list), rewind the file handle and pass the message in to the command. If we # encounter any errors, we bounce the message (we don't want to fail # temporarily, since we've already successfully delivered to the main list). sub feed { my ($message, @command) = @_; # Rewind the temporary file, since the previous operation left it at the # end of the file. sysseek ($message, 0, 0) or bounce 'Unable to rewind temporary file. (#5.3.0)'; # Fork off a child. The child will then reopen STDIN to be the message # and exec the program. local $SIG{PIPE} = 'IGNORE'; my $pid = fork; if (not defined $pid) { bounce 'Unable to fork. (#5.3.0)'; } elsif ($pid == 0) { # In child. Mutilate our file handles. open (STDIN, "<&$$message") or bounce 'Unable to reopen fd 0. (#5.3.0)'; exec @command or bounce "Unable to fork $command[0]. (#5.3.0)"; } # Now, wait for our child and make sure that it succeeded. waitpid ($pid, 0); if ($? != 0) { bounce "$command[0] failed. (#5.3.0)" } } ############################################################################## # Main routine ############################################################################## # We take -r to force VERP to be on and -R to force VERP to be off; if neither # flag is present, we default to whatever $verp is set to at the top of this # file. if ($ARGV[0] =~ /^-/) { my $option = shift; if ($option !~ /^-[rR]+$/) { bounce "Invalid option '$option'. (#5.2.4)"; } elsif ($option =~ /r/ && $option =~ /R/) { bounce 'Only one of -r and -R may be specified. (#5.2.4)'; } $verp = ($option =~ /r/); } # We take two non-optional arguments, the file of addresses to send the # message to and the envelope sender that we should use. We take the message # to be injected on stdin. The filename portion of the address file path is # also taken to be the local e-mail address for the purposes of constructing a # Delivered-To header. (This is a bit of a hack, but it works for the default # majordomo setup.) my ($addresses, $sender) = @ARGV; unless ($sender) { bounce 'Syntax error, sender not given. (#5.2.4)' } unless (-r $addresses) { bounce 'Address file not found. (#5.2.4)' } my ($address) = ($addresses =~ m%/([^/]+)$%); $address .= "\@$hostname"; # Open a temporary file that we'll store the message in. sysopen (TMP, "$tempdir/mjinject.$$", O_CREAT | O_EXCL | O_RDWR) or tempfail 'Unable to create temporary file. (#4.3.0)'; unlink "$tempdir/mjinject.$$" or tempfail 'Unable to unlink temporary file. (#4.3.0)'; # Read in the message and run it through qmail-inject. inject (\*TMP, $address); # Queue the message. queue (\*TMP, $sender, $addresses); # If we need to send the message through archive or digest, do that. if (-r "$addresses.programs") { open (PROGRAMS, "$addresses.programs") or bounce 'Unable to open programs file. (#5.2.4)'; while () { my @command = split; next unless @command; feed (\*TMP, @command); } close PROGRAMS; } # Success. close TMP; exit 0; ############################################################################## # Documentation ############################################################################## =head1 NAME mjinject - Inject a message from Majordomo into the qmail queue =head1 SYNOPSIS B [B<-rR>] I I < I =head1 DESCRIPTION This program is intended to be used as the outgoing mailer for Majordomo's resend program on a system using qmail. It takes a file of addresses and a sender address on the command line and an e-mail message on stdin and passes the message along to qmail-queue after running it through qmail-inject to canonify addresses and the like. In the address file, everything after the first space on a line is ignored, so the addresses can include comments provided that the address is the first sequence of non-whitespace on each line. Whitespace in addresses is not supported. If the file name created by appending C<.programs> to the address file exists, then in addition to passing the message to qmail-inject, mjinject also runs each program listed in that file and passes the message to it. The syntax is one program command line per line of the file, with whitespace separating the arguments (whitespace in arguments is not supported). To use this program as Majordomo's outgoing mailer, just change the $mailer line in F to: $mailer = "/var/qmail/majordomo/mjinject $listdir/$opt_l \$sender"; (replacing /var/qmail/majordomo/mjinject with the full path to this script). Any arguments passed by resend to this program are ignored when using the above syntax, so there's no need to do anything special with the resend invocation. The address list used will be the address list of the mailing list, maintained by Majordomo, so make sure that that address list consists of addresses, one per line, optionally with additional information after the address separated by a space. When using this program, be sure to edit the beginning of it and change the hostname setting to point to your local host. B uses this to fully-qualify addresses. =head1 OPTIONS =over 4 =item B<-r> Force VERP (per-message sender addresses containing the e-mail address of the recipient, to enable cleaner bounce handling) to be used. For more information on VERP, see L. =item B<-R> Force VERP not to be used. =back Without a command-line flag, the VERP default is determined by the setting of $verp at the top of the program (normally on). =head1 RETURN VALUE For nearly all errors, B exits 111 to tell qmail to defer the the message and try to deliver it again. However, if an error is encountered when feeding the message to a program (using a C<.programs> file), the message is bounced instead because it has already been delivered to the subscribers. =head1 SEE ALSO qmail-inject(8), qmail-queue(8) For information about qmail, see L. For detailed information on how to install and use this program, see the Majordomo with qmail FAQ at L. Current versions of this program are available from its web site at L. =head1 AUTHOR Russ Allbery with help from Giles Lean =head1 COPYRIGHT AND LICENSE Copyright 1997, 1998, 2002 by Russ Allbery . This program is free software; you may redistribute it and/or modify it under the same terms as Perl itself. =cut