WWW::Postini - Interact with the Postini mail filtering service


WWW-Postini documentation Contained in the WWW-Postini distribution.

Index


Code Index:

NAME

Top

WWW::Postini - Interact with the Postini mail filtering service

SYNOPSIS

Top

  use WWW::Postini;

  my $p = new WWW::Postini();
  $p->login($login, $password);

DESCRIPTION

Top

This module is an attempt to provide a simple interface to the email quarantine functionality offered by the Postini (http://www.postini.com/) mail filtering service. Behind the scenes, this is achieved by screen-scraping the Postini administration web site.

A NOTE ON EXCEPTIONS

Top

Please note WWW::Postini makes extensive use of Exception::Class objects for improved error handling. Many object methods will throw (read: die()) upon error with a subclass of Exception::Class.

In order to properly handle such errors, it is important to enclose any calls to this module in an eval{} block.

  # try

  eval {

    my $p = new WWW::Postini();
    $p->login($email, $password);

  };

  # catch

  if ($@) {

    if (UNIVERSAL::isa($@, 'Exception::Class')) {

      printf "Caught an exception: %s\n", $@->as_string;

    } else {

      printf "Caught a native error: %s\n", $@;

    }

    exit;  

  }

For more information, please see Exception::Class. Wherever appropriate, this document will detail which subclasses of Exception::Class may be thrown from each method.

CONSTRUCTOR

Top

new()
new($host)

Creates a new instance of WWW::Postini. If $host is specified, it is used as the object's login host, otherwise the default of login.postini.com is used.

  my $p = new WWW::Postini('login2.postini.com');

OBJECT METHODS

Top

user_agent()
user_agent($ua)

Get or set the underlying WWW::Postini::UserAgent object

This method is present in case WWW::Postini needs to be subclassed or the programmer needs access to the user agent itself for other reasons.

  my $user_agent = $p->user_agent();

login_host()
login_host($host)

Get or set login host.

The login host defaults to login.postini.com, unless a host is specified in the constructor. Changing the login host is not necessary at this point, as there is currently only one Postini login server.

admin_host()
admin_host($host)

Get or set administration host.

The administration host is determined automatically during the login() procedure. Until a successful login has taken place, the value of admin_host() will be undefined. It is not necessary to manually set the administration host, but you may if desired. In this case, be sure to set admin_host() after login, but before any other other methods are called.

login($email,$password)

Attempt to login to the Postini mail filtering service with the credentials $email and $password

On failure, this method will throw an instance of the class WWW::Postini::Exception::LoginFailure and it is up to the programmer to catch this exception.

get_user_id($email)

Returns the user ID of the supplied $email argument.

On failure, this method will throw an instance of the class WWW::Postini::Exception::UnexpectedResponse.

list_messages($user_id)
list_messages(%args)
list_messages(\%args)

In its single-argument form, this method will retrieve a list of messages quarantined for the specified $user_id.

If this method is passed a list of key/value pairs or a hash reference, the following keys may be used:

user_id - Target user ID

show - Scope of quarantine listing.

For a listing of values this parameter accepts, please see the "Message searching" section of WWW::Postini::Constants. Defaults to SHOW_ALL

sort - Message sort method

For a listing of values this parameter accepts, please see the "Message sorting" section of WWW::Postini::Constants. Defaults to SORT_NONE

recipient - Narrow searcg by recipient address

Only messages containing this text in the recipient field will be included in the resulting message list. Note: this is a partial text match.

sender - Narrow search by sender address

subject - Narrow search by subject

filter - Narrow search by filter description

On success, this method returns an array reference populated with messages. Each message is a hash reference formatted similar to the following:

  {
    sender    => $sender_address,
    recipient => $recipient_address,
    subject   => $subject,
    date      => $date,
    filter    => $filter_description,
    id        => $message_id,
    uid       => $user_id
  }

On failure, this method will throw a WWW::Postini::Exception::UnexpectedResponse exception.

  use WWW::Postini::Constants ':show';

  # show only quarantined messages

  my $messages = $p->list_messages(
    user_id => $user_id,
    show    => SHOW_QUARANTINED    
  );

  print "Received the following messages\n\n";

  foreach my $msg (@$messages) {

    print "Message ID: $msg->{'id'}\n";

  }

get_message_info($user_id,$message_id)

Retrieve detailed information about the message $message_id belonging to $user_id.

On sucess, a hash reference of the following format will be returned:

  {
    headers     => $header_text,
    body        => $body_text,
    attachments => [ $file1_name, $file2_name, ... ]  
  }

Note: the text returned may be truncated by Postini itself. In addition, attachments will only contain filenames when the current message was blocked due to a disallowed file attachment type.

On failure, this method will throw a WWW::Postini::Exception::UnexpectedResponse exception.

delete_messages($user_id,@messages)
delete_messages($user_id,\@messages)

The specified @messages for $user_id will be marked as deleted.

On success, returns the number of messages successfully deleted.

On failure, this method will throw a WWW::Postini::Exception::UnexpectedResponse exception.

process_messages(%args)
process_messages(\%args)

Process one or more messages. The following parameters are recognized:

user_id - $user_id

recipient - $recipient_value

Specifies where to deliver message. For appropriate values, please see the "Message recipient" section of WWW::Postini::Constants. Defaults to RECIPIENT_USER

mark - 0 or 1

Mark message as delivered. Defaults to 1

clean - 0 or 1

Virus clean before delivering message. Defaults to 1

On success, this method returns the number of messages processed.

On failure, this method will throw a WWW::Postini::Exception::UnexpectedResponse exception.

SEE ALSO

Top

WWW::Postini::Base, WWW::Postini::Constants

AUTHOR

Top

Peter Guzis, <pguzis@cpan.org>

COPYRIGHT AND LICENSE

Top


WWW-Postini documentation Contained in the WWW-Postini distribution.

package WWW::Postini;

use strict;
use warnings;

use WWW::Postini::Assert;
use WWW::Postini::Exception::LoginFailure;
use WWW::Postini::Exception::InvalidParameter;
use WWW::Postini::Exception::UnexpectedResponse;
use WWW::Postini::UserAgent;
use WWW::Postini::Constants ':all';

use HTML::TokeParser;
use HTTP::Request;
use HTTP::Status;

use vars '$VERSION';

use constant DEFAULT_LOGIN_HOST   => 'login.postini.com';

use constant QUARANTINE_PAGE_SIZE => 1000;

use constant LOG_IN_TITLE         => 'Log in';
use constant CHOOSE_SESSION_TITLE => 'Choose Session Type';
use constant ADMIN_TITLE          => 'System Administration';

$VERSION = '0.01';

#################
## constructor ##
#################

sub new {

	my $class = shift;
	my $self = bless {}, $class;
	$self->_init(@_);
	$self;

}

#################
## initializer ##
#################

sub _init {
	
	my $self = shift;

	if (@_) {

		$self->login_host(shift);
		
	} else {
	
		$self->{'_login_host'} = DEFAULT_LOGIN_HOST;
	
	}
	
	$self->{'_user_agent'} = new WWW::Postini::UserAgent();
	return;
	
}

######################
## accessor methods ##
######################

# user agent

sub user_agent {

	my $self = shift;
	
	if (@_) {
	
		$self->{'_user_agent'} = shift;
	
	}
	
	return $self->{'_user_agent'} if defined wantarray;	

}

# login host

sub login_host {
	
	my $self = shift;
	
	if (@_) {
		
		$self->{'_login_host'} = shift;
		
	}
	
	return $self->{'_login_host'} if defined wantarray;
	
}

# admin host

sub admin_host {
	
	my $self = shift;
	
	if (@_) {
		
		$self->{'_admin_host'} = shift;
		
	}
	
	return $self->{'_admin_host'} if defined wantarray;
	
}

#####################
## utility methods ##
#####################

# login

sub login {
	
	my $self = shift;
	
	throw WWW::Postini::Exception::InvalidParameter('Email address not specified')
		unless defined $_[0] && length $_[0]
	;
	throw WWW::Postini::Exception::InvalidParameter('Password not specified')
		unless defined $_[1] && length $_[1]
	;
	
	my $email_address = shift;
	my $password = shift;
	my $response;
	
	# try
	
	eval {
	
		# attempt to log in
		
		$response = $self->{'_user_agent'}->get(
			"https://$self->{'_login_host'}/exec/login?"
			."email=$email_address"
			."&pword=$password"
			."&action=login"
		);

		assert($response, 'Response returned');

		my $code = $response->code();		
		assert($code == RC_OK, 'Response code is '.RC_OK);

		my $content = $response->content();	
		my $p = new HTML::TokeParser(\$content);

		# determine title
		
		my $tag = $p->get_tag('title');
		assert($tag, 'Title tag exists');
		
		my $token = $p->get_token();
		assert($token, 'Token exists');		
		assert($token->[0] eq 'T', 'Token is text');
		assert(
			$token->[1] eq LOG_IN_TITLE || $token->[1] eq CHOOSE_SESSION_TITLE,
			'Title recognized'
		);
		
		# log in
		
		if ($token->[1] eq LOG_IN_TITLE) {

			$tag = $p->get_tag('br');
			assert($tag, 'Break tag exists');
			
			$tag = $p->get_tag('b');
			assert($tag, 'Bold tag exists');
			
			my $text = $p->get_trimmed_text();
			assert($text, 'Bold has text');

			throw WWW::Postini::Exception::LoginFailure($text);

		# choose session (transfer phase)
		
		} elsif ($token->[1] eq CHOOSE_SESSION_TITLE) {
	
			$tag = $p->get_tag('a');
			assert($tag, 'Administration link exists');
			assert($tag->[1]->{'href'}, 'Administration URL defined');

			# make certain user is administrator
			
			my $text = $p->get_trimmed_text();
			assert($text eq ADMIN_TITLE, 'User is administrator');
			
			# determine administration host
			
			my $url = $tag->[1]->{'href'};
			my ($admin_host) = $url =~ m!^https?://(.+?)/!;
			assert($admin_host, 'Administration site determined');
			$self->{'_admin_host'} = $admin_host;
									
			# send transfer code
			
			$response = $self->{'_user_agent'}->get($url);
			
			assert($response, 'Response returned');
		
			$code = $response->code();
			assert($code == RC_OK, 'Response code is '.RC_OK);

		}
		
	};
	
	# catch
	
	if (defined $@ && (length $@ || ref $@)) {

		if (UNIVERSAL::isa($@, 'WWW::Postini::Exception::LoginFailure')) {

			$@->rethrow();
			
		} else {

			throw WWW::Postini::Exception::UnexpectedResponse($@);
		
		}
		
	}

	return 1;
	
}

# get user id

sub get_user_id {
	
	my $self = shift;
	
	throw WWW::Postini::Exception::InvalidParameter('Email address not specified')
		unless defined $_[0] && length $_[0]
	;
	
	my $email_address = shift;
	my ($response, $user_id);
	
	# try
	
	eval {
	
		# avoid automatic redirect so user id can be ascertained
		
		$response = $self->{'_user_agent'}->simple_request(new HTTP::Request (
			GET =>
				'https://'
				.$self->{'_admin_host'}
				.'/exec/adminstart?'
				.'action=user_shortcut'
				."&targetAddress=$email_address"
				.'&next_action=Quarantine'
		));
		
		assert($response, 'Response returned');
		
		my $code = $response->code();
		assert($code == RC_FOUND, 'Response code is '.RC_FOUND);

		my $location = $response->headers->header('location');
		assert($location, 'Location defined');

		($user_id) = $location =~ /targetuserid=(\d+)/;
		assert($user_id, 'User ID defined');
		
	};
	
	# catch
	
	if (defined $@ && (length $@ || ref $@)) {

		if (UNIVERSAL::isa($@, 'WWW::Postini::Exception::UnexpectedResponse')) {

			$@->rethrow();
			
		} else {

			throw WWW::Postini::Exception::UnexpectedResponse($@);
		
		}
		
	}

	$user_id;
	
}

# list messages messages from quarantine

sub list_messages {
	
	my $self = shift;
	my %args;
	
	# handle arguments
	
	if (@_) {
		
		# hash reference
		
		if (defined $_[0] && UNIVERSAL::isa($_[0], 'HASH')) {
		
			%args = %{$_[0]};
		
		# single parameter (user id)
	
		} elsif (@_ == 1) {
			
			$args{'user_id'} = shift;
		
		# array of key/value pairs
		
		} else {
		
			%args = @_;
		
		}
		
	}

	# require user id parameter
	
	throw WWW::Postini::Exception::InvalidParameter ('User ID not specified')
		unless defined $args{'user_id'} && length $args{'user_id'}
	;
	
	# set argument defaults
	
	$args{'show'} = SHOW_ALL unless defined $args{'show'};
	$args{'sort'} = SORT_NONE unless defined $args{'sort'};
	
	# define which messages will be shown
	
	my $show;
	
	if ($args{'show'} == SHOW_ALL) {
		
		$show = 'all';		
		
	} elsif ($args{'show'} == SHOW_QUARANTINED) {
		
		$show = 'visible';
		
	} elsif ($args{'show'} == SHOW_DELIVERED) {
		
		$show = 'delivered';
		
	} elsif ($args{'show'} == SHOW_DELETED) {
		
		$show = 'deleted';
	
	} else {
			
		throw WWW::Postini::Exception::InvalidParameter(
			'Invalid "show" parameter specified'
		);
		
	}

	# define sort method
	
	my $sort;
	
	if ($args{'sort'} == SORT_NONE) {
	
		$sort = '';
		
	} elsif ($args{'sort'} == SORT_RECIPIENT) {
		
		$sort = 'recip';
		
	} elsif ($args{'sort'} == SORT_SENDER) {
		
		$sort = 'sender';
		
	} elsif ($args{'sort'} == SORT_SUBJECT) {
		
		$sort = 'subject';
		
	} elsif ($args{'sort'} == SORT_FILTER) {
	
		$sort = 'block';
	
	}	else {

		throw WWW::Postini::Exception::InvalidParameter(
			'Invalid "sort" parameter specified'
		);
		
	}
	
	my $base_url = "https://$self->{'_admin_host'}/exec/admin_users?"
		."targetuserid=$args{'user_id'}"
		.'&action=display_Quarantine'
	;
	
	my %url_params = (
		pagesize => QUARANTINE_PAGE_SIZE,
		msgtype  => $show,
		msgsort  => $sort
	);
	
	# search for recipient
	
	$url_params{'filterRecip'} = $args{'recipient'}
		if defined $args{'recipient'}
	;	
	
	# search for sender
	
	$url_params{'filterSender'} = $args{'sender'}
		if defined $args{'sender'}
	;

	# search for subject

	$url_params{'filterSubject'} = $args{'subject'}
		if defined $args{'subject'}
	;

	# limit to specified filter

	$url_params{'filterBlock'} = $args{'filter'}
		if defined $args{'filter'}
	;
			
	my $page_number = 1;
	my @messages;

	# try
	
	eval {
		
		my $response;
		
		while (1) {

			# retrieve current page
			
			$response = $self->{'_user_agent'}->post(
				$base_url, [
					%url_params,
					firstmsg => (($page_number - 1) * QUARANTINE_PAGE_SIZE)
				]
			);
		
			assert($response, 'Response returned');
		
			my $code = $response->code();
			assert($code == RC_OK, 'Response code is '.RC_OK);

			my $content = $response->content();
			my $p = new HTML::TokeParser(\$content);	

			my ($tag, $start_range, $end_range, $total_messages);
		
			while (defined ($tag = $p->get_tag('td'))) {

				last if defined $tag->[1]->{'width'}
					&& defined $tag->[1]->{'align'}
					&& $tag->[1]->{'width'} eq '100%'
					&& $tag->[1]->{'align'} eq 'right'
				;
		
			}

			$tag = $p->get_tag('font');
			assert($tag, 'Font tag exists');
			
			my $token;
	
			while (defined ($token = $p->get_token())) {
		
				last if $token->[0] eq 'E' && $token->[1] eq 'font';

				if ($token->[0] eq 'T') {

					next unless $token->[1] =~ /\d+ - \d+ of \d+/s;

					($start_range, $end_range, $total_messages) = $token->[1] =~ /
												(\d+)	\s - \s	(\d+)
												\s of \s
												(\d+)
										/x;
					last;
			
				}
		
			}

			assert(defined ($total_messages), 'Total messages defined');
			assert(defined ($start_range), 'Start range defined');
			assert($start_range <= $total_messages, 'Valid start range');

			$tag = $p->get_tag('form');
			assert($tag, 'Form tag exists');
		
			$tag = $p->get_tag('tr');
			assert($tag, 'TR tag exists');
		
			while($tag = $p->get_tag('tr')) {
			
				next unless defined $tag->[1]->{'bgcolor'} && (
					$tag->[1]->{'bgcolor'} eq '#EEEEEE'
					|| $tag->[1]->{'bgcolor'} eq '#FFFFFF'
				);
				last unless defined $p->get_tag('font');
			
				my ($date, $recipient, $sender) = (
					$self->_get_message_field($p),
					$self->_get_message_field($p),
					$self->_get_message_field($p)
				);
			
				assert($date, 'Date defined');
				assert($recipient, 'Recipient defined');
				assert($sender, 'Sender defined');
			
				$tag = $p->get_tag('font');
				assert($tag, 'Font tag exists');
			
				$tag = $p->get_tag('a');
				assert($tag, 'Link exists');

				my $url = $tag->[1]->{'href'};
				assert($url, 'URL defined');
			
				my $subject = $p->get_trimmed_text();
				assert($subject, 'Subject defined');
			
				my $uri = (split m!\?!, $url)[1];
				assert($uri, 'URI defined');
			
				my %params;

				foreach my $pair (split /&/, $uri) {
			
					my ($key, $value) = split /=/, $pair;
					$params{$key} = $value;	
				
				}
				
				$tag = $p->get_tag('font');
				assert($tag, 'Font tag exists');
				
				my $filter = $p->get_trimmed_text();
				assert($filter, 'Filter defined');

				push @messages, {
					sender    => $sender,
					recipient => $recipient,
					subject   => $subject,
					date      => $date,
					filter    => $filter,
					id        => $params{'msgid'},
					uid       => $args{'user_id'}
				};
			
			}
			
			last if $end_range >= $total_messages;
			$page_number++;
			
		}
		
	};	
	
	# catch
	
	if (defined $@ && (length $@ || ref $@)) {

		if (UNIVERSAL::isa($@, 'WWW::Postini::Exception::UnexpectedResponse')) {

			$@->rethrow();
			
		} else {

			throw WWW::Postini::Exception::UnexpectedResponse($@);
			
		}
		
	}
	
	\@messages;

}

# get message info

sub get_message_info {
	
	my $self = shift;
	
	throw WWW::Postini::Exception::InvalidParameter('User ID not specified')
		unless defined $_[0] && length $_[0]
	;
	throw WWW::Postini::Exception::InvalidParameter('Message ID not specified')
		unless defined $_[1] && length $_[1]
	;
	
	my $user_id = shift;
	my $message_id = shift;
	my ($headers, $body, @attachments);

	eval {
		
		my $response = $self->{'_user_agent'}->get(
			"https://$self->{'_admin_host'}/exec/admin_users?"
			."targetuserid=$user_id"
			.'&action=display_Message'
			."&msgid=$message_id"
			.'&showheader=1'
		);

		assert($response, 'Response returned');
		
		my $code = $response->code();
		assert($code == RC_OK, 'Response code is '.RC_OK);

		my $content = $response->content();
		my $p = new HTML::TokeParser(\$content);

		my $tag = $p->get_tag('pre');
		assert($tag, 'PRE tag exists');

		$headers = $p->get_text();
		assert($headers, 'Headers exist');
		
		$headers =~ s/\s+$//gs;
		
		$tag = $p->get_tag('xmp');
		assert($tag, 'XMP tag exists');
		
		my $token;
		
		while (defined ($token = $p->get_token())) {
		
			# start tag
			
			if ($token->[0] eq 'S') {
			
				$body .= $token->[4];
			
			# end tag
			
			} elsif ($token->[0] eq 'E') {
			
				last if $token->[1] eq 'xmp';
				$body .= $token->[2];

			# text, comment, or declaration
			
			} elsif ($token->[0] eq 'T'
				|| $token->[0] eq 'C'
				|| $token->[0] eq 'D'
			) {
				
				$body .= $token->[1];
				
			# processing instruction
			
			} elsif ($token->[0] eq 'PI') {
			
				$body .= $token->[2];
			
			}		
		
		}
		
		$body =~ s/\s+$//sg;

		my ($text, $file);
		
		while (defined ($token = $p->get_token())) {
			
			last if $token->[0] eq 'E' && $token->[1] eq 'font';
			next unless $token->[0] eq 'T';
			
			if (($file) = $token->[1] =~ /name=\"?([^";]+?)\s*(?:"|\;|$)/s) {

				push @attachments, $file;
				
			}
			
		}				
	
	};

	# catch
	
	if (defined $@ && (length $@ || ref $@)) {

		if (UNIVERSAL::isa($@, 'WWW::Postini::Exception::UnexpectedResponse')) {

			$@->rethrow();
			
		} else {

			throw WWW::Postini::Exception::UnexpectedResponse($@);
		
		}
		
	}

	return {
		headers     => $headers,
		body        => $body,
		attachments => \@attachments	
	};

}

# delete specified messages

sub delete_messages {

	my $self = shift;
	my @message_ids;
	
	throw WWW::Postini::Exception::InvalidParameter('User ID not specified')
		unless defined $_[0] && length $_[0]
	;
	
	my $user_id = shift;
	
	# handle arguments
	
	if (@_) {
	
		# array reference
		
		if (defined $_[0] && UNIVERSAL::isa($_[0], 'ARRAY')) {
	
			@message_ids = @{$_[0]};
		
		# list of values
		
		} else {
			
			@message_ids = @_;		
		
		}
		
	}
	
	throw WWW::Postini::Exception::InvalidParameter('No messages specified')
		unless scalar @message_ids
	;
	
	my $message_count;
		
	# try
	
	eval {
		
		my @args = (
			action       => 'processQuarantine',
			targetuserid => $user_id,
			submit       => 'Delete'
		);
		
		foreach my $id (@message_ids) {
			
			push @args, ('msgid', $id);
			
		}

		my $response = $self->{'_user_agent'}->post(
			"https://$self->{'_admin_host'}/exec/admin_users",
			\@args
		);
		
		assert($response, 'Response returned');
		
		my $code = $response->code();
		assert($code == RC_OK, 'Response code is '.RC_OK);

		my $content = $response->content();
		my $p = new HTML::TokeParser(\$content);
		my ($tag, $text);
		
		while (defined ($tag = $p->get_tag('b'))) {
		
			my $text = $p->get_trimmed_text();			
			last if defined $text && $text eq 'Message Results';			
		
		}

		$tag = $p->get_tag('font');
		assert($tag, 'Font tag exists');
		
		$text = $p->get_trimmed_text();
		assert(
			$text =~ /^\d+ message\(s\) marked as deleted\./,
			'Message text recognized'
		);
		
		($message_count) = $text =~ /^(\d+)/;		
	
	};
	
	# catch
	
	if (defined $@ && (length $@ || ref $@)) {

		if (UNIVERSAL::isa($@, 'WWW::Postini::Exception::UnexpectedResponse')) {

			$@->rethrow();
			
		} else {

			throw WWW::Postini::Exception::UnexpectedResponse($@);
			
		}
		
	}
	
	$message_count;

}

# process quarantined messages

sub process_messages {

	my $self = shift;
	my %args;
	
	# handle arguments
	
	if (@_) {
	
		# array reference
		
		if (defined $_[0] && UNIVERSAL::isa ($_[0], 'HASH')) {
	
			%args = %{$_[0]};
		
		# list of values
		
		} else {
			
			%args = @_;		
		
		}
		
	}
	
	throw WWW::Postini::Exception::InvalidParameter('User ID not specified')
		unless defined $args{'user_id'} && length $args{'user_id'}
	;
	
	# set default parameters
	
	$args{'recipient'} = RECIPIENT_USER unless defined $args{'recipient'};
	$args{'mark'} = 1 unless defined $args{'mark'};
	$args{'clean'} = 1 unless defined $args{'clean'};
	
	# handle recipient parameter
	
	my $recipient;
	
	if ($args{'recipient'} == RECIPIENT_USER) {
	
		$recipient = 'user';
	
	} elsif ($args{'recipient'} == RECIPIENT_ADMIN) {
		
		$recipient = 'admin';	
	
	} else {
	
		throw WWW::Postini::Exception::InvalidParameter(
			'Invalid "recipient" parameter specified'
		);
	
	}
	
	# handle messages parameter
	
	my @message_ids;
	
	if (defined $args{'messages'}) {
	
		if (UNIVERSAL::isa ($args{'messages'}, 'ARRAY')) {
		
			@message_ids = @{$args{'messages'}};
		
		} else {
		
			@message_ids = $args{'messages'};
		
		}
	
	}
	
	# no messages passed
	
	throw WWW::Postini::Exception::InvalidParameter('No messages specified')
		unless scalar @message_ids
	;
	
	my $message_count;
	
	# try
	
	eval {
	
		my @args = (
			action       => 'processQuarantine',
			targetuserid => $args{'user_id'},
			submit       => 'Process',
			markdeliver  => ($args{'mark'} ? 1 : 0),
			preclean     => ($args{'clean'} ? 1 : 0),
			deliv_rcpt   => $recipient
		);
		
		foreach my $id (@message_ids) {
			
			push @args, ('msgid', $id);
			
		}
		
		my $response = $self->{'_user_agent'}->post(
			"https://$self->{'_admin_host'}/exec/admin_users",
			\@args
		);
		
		assert($response, 'Response returned');
		
		my $code = $response->code();
		assert($code == RC_OK, 'Response code is '.RC_OK);

		my $content = $response->content();
		my $p = new HTML::TokeParser(\$content);
		my ($tag, $text);

		while (defined ($tag = $p->get_tag('b'))) {
		
			my $text = $p->get_trimmed_text();
			last if defined $text && $text eq 'Message Results';			
		
		}
		
		$tag = $p->get_tag('font');
		assert($tag, 'Font tag exists');
		
		$text = $p->get_trimmed_text();
		assert(
			$text =~ /^\d+ message\(s\) queued for delivery/,
			'Message text recognized'
		);
		
		($message_count) = $text =~ /^(\d+)/;
	
	};
	
	# catch
	
	if (defined $@ && (length $@ || ref $@)) {

		# unexpected response
		
		if (UNIVERSAL::isa($@, 'WWW::Postini::Exception::UnexpectedResponse')) {

			$@->rethrow;
			
		# all other exceptions
		
		} else {

			throw WWW::Postini::Exception::UnexpectedResponse($@);
			
		}
		
	}
	
	$message_count;

}

# get message field

sub _get_message_field {
	
	my $self = shift;
	my $p = shift;	
	my $tag = $p->get_tag ('font');
	assert ($tag, 'Font tag exists');
	$p->get_trimmed_text;
	
}

1;

__END__