#-*- perl -*-
#
# Copyright (C) 2006 Ken'ichi Fukamachi
# All rights reserved. This program is free software; you can
# redistribute it and/or modify it under the same terms as Perl itself.
#
# $FML: QmailExt.pm,v 1.2 2006/07/09 12:11:12 fukachan Exp $
#
package FML::Command::QmailExt;
use strict;
use vars qw(@ISA @EXPORT @EXPORT_OK $AUTOLOAD);
use Carp;
=head1 NAME
FML::Command::QmailExt - emulate qmail-ext style command parser.
=head1 SYNOPSIS
use FML::Command::QmailExt;
my $extension = new FML::Command::QmailExt $curproc;
if ( $extension->match($ENV{EXT}) ) {
$extension->execute($ENV{EXT});
}
=head1 DESCRIPTION
This class provides qmail-ext style parser and fml8 command execution
wrapper functions.
=head1 METHODS
=head2 new($curproc)
constructor.
=cut
# Descriptions: constructor.
# Arguments: OBJ($self) OBJ($curproc)
# Side Effects: none
# Return Value: OBJ
sub new
{
my ($self, $curproc) = @_;
my ($type) = ref($self) || $self;
my $me = { _curproc => $curproc };
return bless $me, $type;
}
=head2 match($extension)
Environment variable EXT holds extention information.
For example, a mail to ML-subscribe@VIRTUAL.DOMAIN is recognized as
extension with "VIRTUAL.DOMAIN-ML-subscribe" in EXT variable.
=cut
# Descriptions: check if $extension string look a qmail extension command.
# Arguments: OBJ($self) STR($extension)
# Side Effects: none
# Return Value: NUM
sub match
{
my ($self, $extension) = @_;
my $curproc = $self->{ _curproc };
# ASSERT
unless (defined $extension) { return 0;}
unless ($extension) { return 0;}
# 1. parse command normally.
my ($found, $command) = $self->_parse_extension($extension);
$curproc->logdebug("qmail-ext: @$command");
# 2. admin command special handling.
# XXX VERPs and admin command looks same.
# We need more care for admin command, which is an exception.
my ($main_command, $sub_command) = @$command;
if ($main_command =~ /^(admin)$/i) {
if ($sub_command =~ /^[a-z0-9]+$/i) {
$curproc->logdebug("qmail-ext: looks admin command");
}
else {
$curproc->logdebug("qmail-ext: not looks admin command");
$found = 0;
}
}
return $found;
}
# Descriptions: parse extension string.
# return status and command list as ARRAY_REF.
# Arguments: OBJ($self) STR($extension)
# Side Effects: none
# Return Value: ARRAY(NUM, ARRAY_REF)
sub _parse_extension
{
my ($self, $extension) = @_;
my $curproc = $self->{ _curproc };
my $config = $curproc->config();
my $ml_name = $curproc->ml_name();
my $ml_domain = $curproc->ml_domain();
my $anonymous_command_list =
$config->get_as_array_ref('anonymous_command_mail_allowed_commands');
my $user_command_list =
$config->get_as_array_ref('user_command_mail_allowed_commands');
# extension is VIRTUAL.DOMAIN-ML-COMMAND-ARGUMENTS format.
my (@command) = ();
my $found = 0;
COMMAND:
for my $command (@$anonymous_command_list, @$user_command_list) {
my $pattern = sprintf("%s-%s-%s", $ml_domain, $ml_name, $command);
if ($extension =~ /^($pattern)$|^($pattern)\-/i) {
my $argv = $self->_parse_argv($extension, $pattern);
(@command) = ($command, @$argv);
$found = 1;
last COMMAND;
}
}
if ($found) {
return($found, \@command);
}
else {
return(0, []);
}
}
# Descriptions: extract arguments part and return it as ARRAY_REF.
# Arguments: OBJ($self) STR($extension) STR($pattern)
# Side Effects: none
# Return Value: ARRAY_REF
sub _parse_argv
{
my ($self, $extension, $pattern) = @_;
# see qmail-local(5), dot-qmail(5) et.al.
my $argv = $extension;
$argv =~ s/^$pattern//;
$argv =~ s/^\-//;
$argv =~ s/\-\-/\@/g; # XXX @ is unavaialble string. useful for swapping.
$argv =~ s/\-/ /g;
$argv =~ s/\@/-/g;
$argv =~ s/=/\@/g;
if ($argv) {
my (@argv) = split(/\s+/, $argv);
return \@argv;
}
else {
return [];
}
}
=head2 execute($extension)
emulate command process.
=cut
# Descriptions: emulate command process.
# Arguments: OBJ($self) STR($extension)
# Side Effects: bootstrap FML::Process::Command emulation.
# Return Value: none
sub execute
{
my ($self, $extension) = @_;
my $curproc = $self->{ _curproc };
# ASSERT
unless (defined $extension) { return 0;}
unless ($extension) { return 0;}
# 1. parse extension to extract a command (ARRAY_REF).
my ($found, $command) = $self->_parse_extension($extension);
$curproc->log("qmail-ext: emulate command: @$command");
# 2. fake a command request message.
# 2.1 message header is same as the current message.
# 2.2 message body is $command.
my $msg_file = $self->_construct_request_mail($command);
unless (defined $msg_file) {
$curproc->logerror("msg_file emulation failed.");
$curproc->logerror("command execution stop");
return;
}
# 3. close STDIO. re-open STDIO for our faked message.
my $status = $self->_reopen_stdio_channel($msg_file);
unless ($status) {
$curproc->logerror("cannot re-open STDIO.");
$curproc->logerror("command execution stop");
return;
}
# 4. run a new process context by NewProcess() call.
$self->_execute_new_process();
}
# Descriptions: create a temporary request message.
# Arguments: OBJ($self) ARRAY_REF($command)
# Side Effects: none
# Return Value: STR
sub _construct_request_mail
{
my ($self, $command) = @_;
my $curproc = $self->{ _curproc };
my $header = $curproc->incoming_message_header();
# create a new faked message file.
use FileHandle;
my $message_file = $curproc->tmp_file_path();
my $wh = new FileHandle "> $message_file";
if (defined $wh) {
$wh->autoflush(1);
$header->print($wh);
print $wh "\n";
print $wh join(" ", @$command), "\n";
$wh->close();
}
else {
$curproc->logerror("cannot open tmp file: $message_file");
return undef;
}
if (-f $message_file && -s $message_file) {
return $message_file;
}
else {
return undef;
}
}
# Descriptions: close and re-open STDIN
# Arguments: OBJ($self) STR($message_file)
# Side Effects: close and re-open STDIN
# Return Value: NUM
sub _reopen_stdio_channel
{
my ($self, $message_file) = @_;
close(STDIN);
my $status = open(STDIN, $message_file);
return( $status ? 1 : 0 );
}
# Descriptions: emulate execution of command mail process.
# Arguments: OBJ($self)
# Side Effects: execute a new process.
# Return Value: none
sub _execute_new_process
{
my ($self) = @_;
my $curproc = $self->{ _curproc };
my $myname = "command";
my $ml_name = $curproc->ml_name();
my $ml_domain = $curproc->ml_domain();
$curproc->logdebug("emulate $myname for $ml_name\@$ml_domain ML");
my $hints = {
config_overload => {
'use_incoming_mail_header_loop_check' => 'no',
},
};
eval q{
use FML::Process::Switch;
&FML::Process::Switch::NewProcess($curproc,
$myname,
$ml_name,
$ml_domain,
$hints);
};
if ($@) {
$curproc->logerror($@);
}
$curproc->logdebug("emulation done");
}
=head1 CODING STYLE
See C on fml coding style guide.
=head1 AUTHOR
Ken'ichi Fukamachi
=head1 COPYRIGHT
Copyright (C) 2006 Ken'ichi Fukamachi
All rights reserved. This program is free software; you can
redistribute it and/or modify it under the same terms as Perl itself.
=head1 HISTORY
FML::Command::QmailExt appeared in fml8 mailing list driver package.
See C for more details.
=cut
1;