1275 lines
41 KiB
Perl
1275 lines
41 KiB
Perl
# ====================================================================
|
|
# Gossamer Threads Module Library - http://gossamer-threads.com/
|
|
#
|
|
# GT::Mail::Parts
|
|
# Author : Scott Beck
|
|
# CVS Info : 087,071,086,086,085
|
|
# $Id: Parts.pm,v 1.84 2006/11/04 19:51:54 brewt Exp $
|
|
#
|
|
# Copyright (c) 2004 Gossamer Threads Inc. All Rights Reserved.
|
|
# ====================================================================
|
|
#
|
|
|
|
package GT::Mail::Parts;
|
|
# ===================================================================
|
|
|
|
# Pragmas
|
|
use strict;
|
|
use vars qw($VERSION @ISA $FIELD_NAME $PARAMNAME $FIRST $TSPECIAL $TOKEN $SPCZ $CRLF @HEADER $ERRORS $DEBUG);
|
|
use GT::Base;
|
|
use GT::Text::Tools;
|
|
|
|
$VERSION = sprintf "%d.%03d", q$Revision: 1.84 $ =~ /(\d+)\.(\d+)/;
|
|
@ISA = qw(GT::Base);
|
|
$DEBUG = 0;
|
|
$CRLF = "\015\012";
|
|
@HEADER = qw/Return-Path Received Date From Subject Sender To Cc Bcc Content-Type Content-Transfer-Encoding Content-Disposition X-Mailer Message-Id/;
|
|
|
|
$ERRORS = {};
|
|
|
|
# Pattern to match a RFC822 Field name ( Extract from RFC #822)
|
|
my $FIELD_NAME = '[^\x00-\x1f\x7f-\xff :]+:';
|
|
|
|
# Pattern to match parameter names (like fieldnames, but = not allowed):
|
|
my $PARAMNAME = '[^\x00-\x1f\x7f-\xff :=]+';
|
|
|
|
# Pattern to match the first value on the line:
|
|
my $FIRST = '[^\s\;\x00-\x1f\x7f-\xff]+';
|
|
|
|
# Pattern to match an RFC-1521 token:
|
|
#
|
|
# token = 1*<any (ASCII) CHAR except SPACE, CTLs, or tspecials> #
|
|
my $TSPECIAL = '()<>@,;:\</[]?="';
|
|
my $TOKEN = '[^ \x00-\x1f\x7f-\xff' . "\Q$TSPECIAL\E" . ']+';
|
|
|
|
# Pattern to match spaces or comments:
|
|
my $SPCZ = '(?:\s|\([^\)]*\))*';
|
|
|
|
sub new {
|
|
# --------------------------------------------------------------------------
|
|
# my $obj = $class->new;
|
|
# ----------------------
|
|
# Generic constructor.
|
|
#
|
|
my ($self, %opts) = @_;
|
|
my $type = ref($self) || $self;
|
|
$self = bless {
|
|
header_lines => {},
|
|
header_order => [],
|
|
headers_intact => $opts{headers_intact} || 0,
|
|
parts => [],
|
|
_debug => $opts{debug},
|
|
header_charset => $opts{header_charset} || 'ISO-8859-1',
|
|
}, $type;
|
|
$self;
|
|
}
|
|
|
|
##################################################
|
|
## Header Methods ##
|
|
##################################################
|
|
|
|
sub extract {
|
|
# --------------------------------------------------------------------------
|
|
# $obj->extract(\@headerline);
|
|
# ----------------------------
|
|
# Takes an array reference containing the header lines and parses it into
|
|
# an internal data structure accessible with methods in this class.
|
|
#
|
|
my ($self, $headlines) = @_;
|
|
my $lines;
|
|
|
|
# Remove any starting header lines which are really broken
|
|
shift @$headlines while @$headlines > 0 and $headlines->[0] !~ /^(?:$FIELD_NAME|From )/o;
|
|
|
|
while (@$headlines > 0 and $headlines->[0] =~ /^($FIELD_NAME|From )/o) {
|
|
my $tag = $1;
|
|
|
|
$lines = shift @$headlines;
|
|
$lines .= shift @$headlines while @$headlines > 0 and $headlines->[0] =~ /^[ \t]+/;
|
|
|
|
# Remove any following header lines which are really broken
|
|
shift @$headlines while @$headlines > 0 and $headlines->[0] !~ /^$FIELD_NAME/o;
|
|
|
|
($tag, $lines) = $self->_fmt_line($tag, $lines) or return;
|
|
$self->_insert($tag, $lines) if defined $lines;
|
|
}
|
|
shift @$headlines if @$headlines > 0 and $headlines->[0] =~ /^\s*$/;
|
|
|
|
return $self;
|
|
}
|
|
|
|
sub parse_params {
|
|
# --------------------------------------------------------------------------
|
|
# my $params = $obj->parse_params($raw);
|
|
# --------------------------------------
|
|
# Takes a raw line from the header and returns a hash ref of param name to
|
|
# it's value. The initial value is stored with key _.
|
|
#
|
|
my ($self, $raw) = @_;
|
|
my $params = {};
|
|
my $param;
|
|
|
|
# Get raw field, and unfold it:
|
|
defined $raw or $raw = '';
|
|
$raw =~ tr/\n//d;
|
|
|
|
# Extract special first parameter:
|
|
$raw =~ m/\A$SPCZ($FIRST)$SPCZ/og or return {}; # nada!
|
|
$params->{'_'} = $1;
|
|
|
|
# Extract subsequent parameters.
|
|
# No, we can't just "split" on semicolons: they're legal in quoted strings!
|
|
while (1) { # keep chopping away until done...
|
|
$raw =~ m/$SPCZ\;$SPCZ/og or last; # skip leading separator
|
|
$raw =~ m/($PARAMNAME)\s*=\s*/og or last; # give up if not a param
|
|
$param = lc($1);
|
|
$raw =~ m/"((?:[^\\"]+|\\.)+)"|($TOKEN)/g or last; # give up if no value
|
|
my ($quoted, $unquoted) = ($1, $2);
|
|
$quoted =~ s/\\(.)/$1/g if $quoted;
|
|
$params->{$param} = $quoted || $unquoted;
|
|
}
|
|
|
|
return $params;
|
|
}
|
|
|
|
sub effective_type {
|
|
# --------------------------------------------------------------------------
|
|
# $obj->effective_type;
|
|
# ---------------------
|
|
# $obj->effective_type('text/plain');
|
|
# -----------------------------------
|
|
# This is a set or get method for the effective type. If there is no
|
|
# effective type this method returns the mime type.
|
|
#
|
|
my $self = shift;
|
|
$self->{efftype} = shift if @_;
|
|
return ($self->{efftype} ? lc($self->{efftype}) : $self->mime_type);
|
|
}
|
|
|
|
sub get {
|
|
# --------------------------------------------------------------------------
|
|
# my @to = $obj->get('to');
|
|
# -------------------------
|
|
# my $from = $obj->get('from');
|
|
# -----------------------------
|
|
# my $recieved = $obj->get('recieved', 1);
|
|
# ----------------------------------------
|
|
# This provides a method to access the header tags. In list context this
|
|
# always returns an array of all the specified tags.
|
|
# In scalar context returns the tag's value specified by position (second
|
|
# argument). If no position is specified returns the first accurence.
|
|
#
|
|
my ($self, $tag, $pos) = @_;
|
|
$pos ||= 0;
|
|
unless (defined $tag) {
|
|
return keys %{$self->{header_lines}};
|
|
}
|
|
$tag = lc $tag;
|
|
exists $self->{header_lines}->{$tag} or return;
|
|
my @name = @{$self->{header_lines}->{$tag}};
|
|
return wantarray ? @name : $name[$pos];
|
|
}
|
|
|
|
sub set {
|
|
# --------------------------------------------------------------------------
|
|
# $obj->set( to => 'scott@gossamer-threads.com', $pos );
|
|
# ------------------------------------------------
|
|
# This method allows you to set tags in the header. If you do not specify a
|
|
# position (third argument) all the tags of that name are deleted and what
|
|
# you specify for that tag is put in its place.
|
|
#
|
|
my ($self, $tag, $val, $pos) = @_;
|
|
defined $tag or return $self->error("BADARGS", "FATAL", '$obj->set($tag, $value, $pos); $pos is optional but $tag and $value is not');
|
|
defined $val or return $self->error("BADARGS", "FATAL", '$obj->set($tag, $value, $pos); $pos is optional but $tag and $value is not');
|
|
$tag = lc($tag);
|
|
unless (defined $pos) {
|
|
@{$self->{header_lines}->{$tag}} = ($val);
|
|
for (my $i = 0; $i < @{$self->{header_order}}; $i++) {
|
|
if (lc $self->{header_order}->[$i]->[0] eq $tag) {
|
|
splice @{$self->{header_order}}, $i, 1;
|
|
$i--;
|
|
}
|
|
}
|
|
push @{$self->{header_order}}, [$self->_tag_case($tag), 0];
|
|
}
|
|
else {
|
|
$self->{header_lines}->{$tag}->[$pos] = $val;
|
|
my $found;
|
|
for (@{$self->{header_order}}) {
|
|
if (lc $_->[0] eq $tag and $_->[1] == $pos) {
|
|
$found++;
|
|
last;
|
|
}
|
|
}
|
|
push @{$self->{header_order}}, [$self->_tag_case($tag), $pos] unless $found;
|
|
}
|
|
return 1;
|
|
}
|
|
|
|
sub add {
|
|
# --------------------------------------------------------------------------
|
|
# $obj->add(To => 'scott@gossamer-threads.com', $prepend);
|
|
# ----------------------------------------------
|
|
# This method allows you to add a line to the headers. Unlike set(), this
|
|
# will not delete any existing headers with the same name and does not
|
|
# require a position. If you pass in a true value for $prepend, then the
|
|
# header will be prepended to the top of the headers.
|
|
#
|
|
my ($self, $tag, $val, $prepend) = @_;
|
|
defined $tag or return $self->error("BADARGS", "FATAL", '$obj->add($tag, $value)');
|
|
defined $val or return $self->error("BADARGS", "FATAL", '$obj->add($tag, $value)');
|
|
$tag = lc($tag);
|
|
|
|
if (exists $self->{header_lines}->{$tag}) {
|
|
if ($prepend) {
|
|
unshift @{$self->{header_lines}->{$tag}}, $val;
|
|
}
|
|
else {
|
|
push @{$self->{header_lines}->{$tag}}, $val;
|
|
}
|
|
}
|
|
else {
|
|
@{$self->{header_lines}->{$tag}} = ($val);
|
|
}
|
|
if ($prepend) {
|
|
for (@{$self->{header_order}}) {
|
|
if (lc $_->[0] eq $tag) {
|
|
$_->[1]++;
|
|
}
|
|
}
|
|
unshift @{$self->{header_order}}, [$self->_tag_case($tag), 0];
|
|
}
|
|
else {
|
|
push @{$self->{header_order}}, [$self->_tag_case($tag), $#{$self->{header_lines}->{$tag}}];
|
|
}
|
|
return 1;
|
|
}
|
|
|
|
sub delete {
|
|
# --------------------------------------------------------------------------
|
|
# $obj->delete('to');
|
|
# -------------------
|
|
# This method allows you to delete tags in the header of the message. If
|
|
# there are more than one of the specified tag they are all deleted. e.g.
|
|
# there is often more than one Received tag in a header. If you specify that
|
|
# tag all of them are deleted.
|
|
#
|
|
# $obj->delete('received', $pos);
|
|
# -------------------------------
|
|
# This allows you to delete tages at a certain position. e.g. If you want to
|
|
# delete the second Received tag you would do:
|
|
# $obj->delete('received', 1);
|
|
# Note that position is zero based.
|
|
# Returns whatever was deleted or spliced.
|
|
#
|
|
my $self = shift;
|
|
defined $_[0] or return $self->error("BADARGS", "FATAL", '$obj->delete($tag)');
|
|
my $tag = lc($_[0]);
|
|
if (defined $_[1] and exists $self->{header_lines}->{$tag}) {
|
|
my $del = splice(@{$self->{header_lines}->{$tag}}, $_[1], 1);
|
|
return wantarray ? @$del : $del->[0];
|
|
}
|
|
elsif (exists $self->{header_lines}->{$tag} and not defined $_[1]) {
|
|
my $del = delete $self->{header_lines}->{$tag};
|
|
return wantarray ? @$del : $del->[0];
|
|
}
|
|
else {
|
|
return;
|
|
}
|
|
}
|
|
|
|
sub size {
|
|
# --------------------------------------------------------------------------
|
|
# my $part_size = $obj->size;
|
|
# ---------------------------
|
|
# Returns the total size of the part including the body.
|
|
#
|
|
my $self = shift;
|
|
my $size = 0;
|
|
if ($self->{body_in}) {
|
|
if ($self->{body_in} eq 'MEMORY') {
|
|
$size = length $self->{data};
|
|
}
|
|
elsif ($self->{body_in} eq 'HANDLE') {
|
|
$size = -s $self->{io};
|
|
}
|
|
elsif ($self->{body_in} eq 'FILE') {
|
|
$size = -s $self->{path};
|
|
}
|
|
}
|
|
$size += length $self->header_as_string;
|
|
return $size;
|
|
}
|
|
|
|
sub preamble {
|
|
# --------------------------------------------------------------------------
|
|
# my $preamble = $obj->preamble;
|
|
# ------------------------------
|
|
# $obj->preamble("My cool preamble");
|
|
# -----------------------------------
|
|
# This is a set or get method for the preamble of this part.
|
|
#
|
|
my ($self, $lines) = @_;
|
|
$self->{preamble} = $lines if @_ > 1;
|
|
return $self->{preamble};
|
|
}
|
|
|
|
sub epilogue {
|
|
# --------------------------------------------------------------------------
|
|
# my $epilogue = $obj->epilogue;
|
|
# ------------------------------
|
|
# $obj->epilogue("My cool epilogue");
|
|
# -----------------------------------
|
|
# This is a set or get method for the parts epilogue.
|
|
#
|
|
my ($self, $lines) = @_;
|
|
$self->{epilogue} = $lines if @_ > 1;
|
|
return $self->{epilogue};
|
|
}
|
|
|
|
sub header_charset {
|
|
# -----------------------------------------------------------------------------
|
|
# my $charset = $obj->header_charset;
|
|
# -----------------------------------
|
|
# $obj->header_charset('windows-1256');
|
|
# -------------------------------------
|
|
# This sets or retrieves the charset that will be used to encode the header.
|
|
# The default encoding is ISO-8859-1.
|
|
#
|
|
my $self = shift;
|
|
$self->{header_charset} = shift if @_;
|
|
return $self->{header_charset};
|
|
}
|
|
|
|
sub binmode {
|
|
# --------------------------------------------------------------------------
|
|
# $obj->binmode(1);
|
|
# -----------------
|
|
# Sets the binmode flag for all opens done on the body of the message.
|
|
# If this flag is set binmode will be used on all files.
|
|
#
|
|
my $self = shift;
|
|
my $mode = shift;
|
|
$self->{mode} = $mode if defined $mode;
|
|
return $self->{mode};
|
|
}
|
|
|
|
sub mime_type {
|
|
# --------------------------------------------------------------------------
|
|
# my $type = $obj->mime_type;
|
|
# ---------------------------
|
|
# $obj->mime_type($default_type);
|
|
# -------------------------------
|
|
# Returns the content-type of this message. If there is no content type
|
|
# and you passed in a default, returns that.
|
|
#
|
|
my ($self, $default) = @_;
|
|
$self->{mime_type} = $default if @_ > 1;
|
|
return lc($self->mime_attr('content-type') || $self->{mime_type} || $self->get('content-type'));
|
|
}
|
|
|
|
sub is_multipart {
|
|
# --------------------------------------------------------------------------
|
|
# $obj->is_multipart;
|
|
# -------------------
|
|
# Returns true if this part is a multipart part. Returns false otherwise.
|
|
#
|
|
my $self = shift;
|
|
my ($type, $subtype) = split('/', $self->mime_type);
|
|
$type ||= 'text';
|
|
$subtype ||= 'plain';
|
|
return (($type eq 'multipart') ? 1 : 0);
|
|
}
|
|
|
|
sub parts {
|
|
# --------------------------------------------------------------------------
|
|
# my @parts = $obj->parts;
|
|
# ------------------------
|
|
# Returns an array of part objects if there are any parts to this part.
|
|
# Returns false if there are no parts.
|
|
#
|
|
my $self = shift;
|
|
$self->{parts} ||= [];
|
|
if (@_ > 0) {
|
|
push @{$self->{parts}}, @_;
|
|
unless ($self->multipart_boundary) {
|
|
$self->multipart_boundary("---------=_" . scalar(time) . "-$$-" . int(rand(time)/2));
|
|
}
|
|
}
|
|
return wantarray ? @{$self->{parts}} : $self->{parts};
|
|
}
|
|
|
|
sub mime_encoding {
|
|
# --------------------------------------------------------------------------
|
|
# my $encoding = $obj->mime_encoding;
|
|
# -----------------------------------
|
|
# Returns the content-transfer-encoding for the current part. This method
|
|
# returns 7bit if there is none.
|
|
#
|
|
my $self = shift;
|
|
return lc($self->mime_attr('content-transfer-encoding') || '7bit');
|
|
}
|
|
|
|
sub multipart_boundary {
|
|
# --------------------------------------------------------------------------
|
|
# my $boundary = $obj->multipart_boundary;
|
|
# ----------------------------------------
|
|
# Set get method for the multipart boundary to use if this part is a
|
|
# multipart message.
|
|
#
|
|
my $self = shift;
|
|
my $boundary = shift;
|
|
if (defined $boundary) {
|
|
$self->{boundary} = $boundary;
|
|
}
|
|
elsif ($self->mime_attr('content-type.boundary')) {
|
|
$self->{boundary} = $self->mime_attr('content-type.boundary');
|
|
}
|
|
return $self->{boundary};
|
|
}
|
|
|
|
sub mime_attr {
|
|
# --------------------------------------------------------------------------
|
|
# $obj->mime_attr('type.subtype');
|
|
# --------------------------------
|
|
# Given a string that consists of type.sybtype returns the value of
|
|
# subtype. If only type is sent returns the value of type. This is useful
|
|
# when dealing with header fields such as Content-Type where it can have
|
|
# a subtype like:
|
|
# Content-Type: text/plain; name="foo.js"
|
|
#
|
|
|
|
my ($self, $attr) = @_;
|
|
|
|
# Break attribute name up:
|
|
my ($tag, $subtag) = split /\./, $attr;
|
|
$subtag ||= '_';
|
|
defined($tag) or return $self->error("BADARGS", "FATAL", '$obj->mime_attr ("type.subtype")');
|
|
|
|
$tag = lc($tag);
|
|
|
|
# There should only be one of these.
|
|
defined($self->{header_lines}->{$tag}->[-1]) or return;
|
|
my $field = $self->parse_params($self->{header_lines}->{$tag}->[-1]);
|
|
return $field->{$subtag};
|
|
}
|
|
|
|
sub decode_mimewords {
|
|
# --------------------------------------------------------------------------
|
|
# my $line = decode_mimewords($line);
|
|
# -----------------------------------
|
|
# Checks $line for mime encodings and decodes them if found. The string
|
|
# passwed in should be a line in the header.
|
|
#
|
|
my $encstr = shift;
|
|
my @tokens;
|
|
$@ = ''; # error-return
|
|
|
|
$encstr =~ s{(\?\=)\r?\n[ \t](\=\?)}{$1$2}gs;
|
|
pos($encstr) = 0;
|
|
|
|
# Decode:
|
|
my ($charset, $encoding, $enc, $dec);
|
|
while (1) {
|
|
last if (pos($encstr) >= length($encstr));
|
|
my $pos = pos($encstr); # save it
|
|
|
|
# Case 1: are we looking at "=?..?..?="?
|
|
if ($encstr =~ m{\G # from where we left off..
|
|
=\?([^?]*) # "=?" + charset +
|
|
\?([bq]) # "?" + encoding +
|
|
\?([^?]+) # "?" + data maybe with spcs +
|
|
\?= # "?="
|
|
}xgi) {
|
|
($charset, $encoding, $enc) = ($1, lc($2), $3);
|
|
$dec = (($encoding eq 'q') ? _decode_q($enc) : _decode_b($enc));
|
|
push @tokens, [$dec, $charset];
|
|
next;
|
|
}
|
|
|
|
# Case 2: are we looking at a bad "=?..." prefix?
|
|
# We need this to detect problems for case 3, which stops at "=?":
|
|
pos($encstr) = $pos; # reset the pointer.
|
|
if ($encstr =~ m{\G=\?}xg) {
|
|
$@ .= qq|unterminated "=?..?..?=" in "$encstr" (pos $pos)\n|;
|
|
push @tokens, ['=?'];
|
|
next;
|
|
}
|
|
|
|
# Case 3: are we looking at ordinary text?
|
|
pos($encstr) = $pos; # reset the pointer.
|
|
if ($encstr =~ m{\G # from where we left off...
|
|
([\x00-\xFF]*? # shortest possible string,
|
|
\n*) # followed by 0 or more NLs,
|
|
(?=(\Z|=\?)) # terminated by "=?" or EOS
|
|
}xg) {
|
|
length($1) or die "GT::Mail::Parts: internal logic err: empty token.";
|
|
push @tokens, [$1];
|
|
next;
|
|
}
|
|
|
|
# Case 4: bug!
|
|
die "GT:Mail::Parts: unexpected case: ($encstr) pos $pos Please alert developer.";
|
|
}
|
|
return (wantarray ? @tokens : join('', map { $_->[0] } @tokens));
|
|
}
|
|
|
|
sub encode_mimewords {
|
|
# --------------------------------------------------------------------------
|
|
# my $line = decode_mimewords($line);
|
|
# -----------------------------------
|
|
# Checks $line for non-ansii and encodes them if found. The string
|
|
# passed in should be a line in the header.
|
|
#
|
|
my $words = shift;
|
|
return $words if $words =~ /\A[\x09\x0A\x0D\x20-\x7E]*\Z/;
|
|
my $encoding = uc(shift || 'Q');
|
|
my $charset = uc(shift || 'ISO-8859-1');
|
|
$words =~ s{([a-zA-Z0-9\x7F-\xFF]{1,18})}{
|
|
my $word = $1;
|
|
my $ret;
|
|
if ($word =~ /\A[\x09\x0A\x0D\x20-\x7E]*\Z/o) {
|
|
$ret = $word;
|
|
}
|
|
else {
|
|
$ret = "=?$charset?$encoding?" . ($encoding eq 'Q' ? _encode_q($word) : _encode_b($word)) . '?=';
|
|
}
|
|
|
|
# As per rfc2047 section 2, we can not have lines more then 75 characters long
|
|
# we must seperate long encodings with a CRLF and space.
|
|
if (length $word == 18 and substr($words, pos($words) + 18, 1) =~ /\S/) {
|
|
$ret .= $CRLF . " ";
|
|
}
|
|
$ret;
|
|
}eg;
|
|
# Spaces between encoded words need to be encoded as well, or they won't show up
|
|
$words =~ s/\?=( +)=\?/"?= =?$charset?Q?" . ("=20" x length $1) . "?= =?"/eg;
|
|
return $words;
|
|
}
|
|
|
|
sub fold {
|
|
# --------------------------------------------------------------------------
|
|
my ($tag, $line) = @_;
|
|
|
|
# Remove any newlines that shouldn't be in the header
|
|
$line =~ s/(?:\r?\n)+$//;
|
|
$line =~ s/(?:\r?\n)(?=[^\t ])/ /g;
|
|
|
|
# Don't fold if we don't need to
|
|
my $fold = 0;
|
|
for (split /$CRLF/, $line) {
|
|
if (length $_ > 72) {
|
|
$fold = 1;
|
|
last;
|
|
}
|
|
}
|
|
return $line unless $fold;
|
|
|
|
# First we unfold
|
|
$line =~ s/[\t ]*[$CRLF][\t ]*/ /g;
|
|
my $key = lc $tag;
|
|
|
|
# Special case for TO fields
|
|
if ($key eq 'to' or $key eq 'cc' or $key eq 'bcc') {
|
|
$line =~ s/,[\t ]+/,$CRLF /g if length $line > 72;
|
|
return $line;
|
|
}
|
|
|
|
# We have nothing to wrap on :(
|
|
if ($line !~ /\s/) {
|
|
return $line;
|
|
}
|
|
|
|
# Wrap the line
|
|
$line = GT::Text::Tools->linewrap($line, 72, { eol => $CRLF, nowrap => [ q{=\?([^?]*)\?([bq])\?([^?]+)\?=} ] });
|
|
|
|
# Outlook has a problem with unfolding subject lines with more then one leading
|
|
# space. It ends up putting that into the actual subject.
|
|
$line =~ s/^/ /mg;
|
|
$line =~ s/^\s+//;
|
|
$line =~ s/$CRLF$//;
|
|
return $line;
|
|
}
|
|
|
|
sub header_as_string {
|
|
# --------------------------------------------------------------------------
|
|
# my $header = $obj->header_as_string;
|
|
# ------------------------------------
|
|
# Returns the header for this part as a string. This actually creates the
|
|
# header when called so it has a bit of overhead.
|
|
#
|
|
my ($self) = @_;
|
|
my $ret = '';
|
|
my %head = %{$self->{header_lines}};
|
|
|
|
if ($self->{headers_intact}) {
|
|
for (@{$self->{header_order}}) {
|
|
my ($tag, $j) = @{$_};
|
|
my $ltag = lc $tag;
|
|
if (exists $head{$ltag} and defined $head{$ltag}->[$j]) {
|
|
$ret .= "$tag: " . encode_mimewords($head{$ltag}->[$j], undef, $self->{header_charset}) . $CRLF;
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
foreach my $tag (@HEADER) {
|
|
my $key = lc $tag;
|
|
$tag = $self->_tag_case($tag);
|
|
if ($key eq 'content-type') {
|
|
if ($self->get($tag)) {
|
|
my $val = $self->get($key);
|
|
$ret .= "$tag: " . fold($tag, encode_mimewords($val, undef, $self->{header_charset})) . $CRLF;
|
|
}
|
|
elsif ($self->mime_type) {
|
|
my $val = $self->mime_type;
|
|
$ret .= "$tag: " . fold($tag, encode_mimewords($val, undef, $self->{header_charset})) . $CRLF;
|
|
}
|
|
else {
|
|
$ret .= "$tag: text/plain$CRLF";
|
|
}
|
|
delete $head{$key};
|
|
next;
|
|
}
|
|
next unless exists $self->{header_lines}{$key};
|
|
foreach (@{$self->{header_lines}->{$key}}) {
|
|
next unless defined $_;
|
|
$ret .= "$tag: " . fold($tag, $key eq 'received' ? $_ : encode_mimewords($_, undef, $self->{header_charset})) . $CRLF; # Can not encode recieved lines
|
|
}
|
|
delete $head{$key};
|
|
}
|
|
for my $key (sort keys %head) {
|
|
next unless defined $key;
|
|
my $tag = $self->_tag_case($key);
|
|
foreach (@{$self->{header_lines}{$key}}) {
|
|
next unless defined $_;
|
|
$ret .= "$tag: " . fold($tag, encode_mimewords($_, undef, $self->{header_charset})) . $CRLF;
|
|
}
|
|
}
|
|
}
|
|
return $ret;
|
|
}
|
|
|
|
sub split_field {
|
|
# --------------------------------------------------------------------------
|
|
# my @to = $obj->split_field('to');
|
|
# ---------------------------------
|
|
# Takes the tag you wish to split on and returns the parts split on
|
|
# \s*,\s*. This is usfull only for fields that have comma seperated
|
|
# values such as emails. The split know about quotes and will not split
|
|
# inside quotes. Returns the number of parts in scalar context, the
|
|
# list of parts in list context.
|
|
#
|
|
my ($self, $tag) = @_;
|
|
defined($tag) or $tag = 'to';
|
|
$tag = lc($tag);
|
|
exists($self->{header_lines}->{$tag}) or return wantarray ? () : 0;
|
|
(@{$self->{header_lines}->{$tag}} > 0) or return wantarray ? () : 0;
|
|
my @ret;
|
|
for my $val (@{$self->{header_lines}->{$tag}}) {
|
|
push @ret, GT::Mail::Parts->split_line('\s*,\s*', $val);
|
|
}
|
|
return @ret;
|
|
}
|
|
|
|
sub split_line {
|
|
# --------------------------------------------------------------------------
|
|
# Class->split_line('\s*,\s*', $line);
|
|
# -----------------------------
|
|
# Splits a line given a delimitor regex and the line. Returns an array
|
|
# of the peices
|
|
#
|
|
# We will be testing undef strings
|
|
my $class = shift;
|
|
local $^W;
|
|
|
|
my ($delimiter, $line) = @_;
|
|
$delimiter =~ s/(\s)/\\$1/g;
|
|
|
|
my ($quote, $quoted, $unquoted, $delim, $word, @pieces);
|
|
|
|
@pieces = $line =~ m{
|
|
\G
|
|
(
|
|
# GIANT HACK WARNING!
|
|
# To keep Perl from segfaulting, simply skip over any chunks of
|
|
# 8000 characters without a delimiter, up to the next delimiter.
|
|
# See segfault warning below. This isn't perfect, but such an
|
|
# e-mail is probably bogus anyway.
|
|
(?: . (?! $delimiter ) ){8000,}
|
|
|
|
|
(?:
|
|
" (?:\\.|[^\\"]+(?=[\\"])) * "
|
|
|
|
|
\s ' (?:\\.|[^\\']+(?=[\\'])) * ' # Hack to get around email addresses with ' in them
|
|
|
|
|
\\.
|
|
|
|
|
.
|
|
)+? # May segfault on very long lines (around 8000+ iterations of this group)
|
|
)
|
|
(?: $ | (?:$delimiter)+ )
|
|
}gsx;
|
|
return @pieces;
|
|
}
|
|
|
|
########################################################
|
|
## Body Methods ##
|
|
########################################################
|
|
|
|
sub suggest_encoding {
|
|
# --------------------------------------------------------------------------
|
|
# my $encoding = $obj->suggest_encoding;
|
|
# --------------------------------------
|
|
# This method attempts to guess an encoding for this part. It does this
|
|
# by looking at the mime type and possibly even scaning the body of the
|
|
# part.
|
|
#
|
|
my $self = shift;
|
|
my $eff_type = $self->effective_type || '';
|
|
my ($type) = split('/', $eff_type);
|
|
$type ||= 'text';
|
|
$self->{body_in} ||= '';
|
|
if ((($type eq 'text') || ($type eq 'message'))) { # scan message body
|
|
if ($self->{body_in} eq 'MEMORY') {
|
|
return 'quoted-printable';
|
|
}
|
|
my ($IO, $unclean);
|
|
if ($self->{body_in} eq 'FILE') {
|
|
$IO = $self->open("r");
|
|
}
|
|
elsif ($self->{body_in} eq 'HANDLE') {
|
|
$IO = $self->{io};
|
|
}
|
|
else {
|
|
return 'binary';
|
|
}
|
|
|
|
# Scan message for 7bit-cleanliness:
|
|
while (<$IO>) {
|
|
last if ($unclean = ((length($_) > 999) or /[\200-\377]/));
|
|
}
|
|
# Reset our handle back to the beginning.
|
|
seek $IO, 0, 0;
|
|
|
|
# Return '7bit' if clean; try and encode if not...
|
|
# Note that encodings are not permitted for messages!
|
|
return (($type eq 'message') ? 'binary' : 'quoted-printable');
|
|
}
|
|
else {
|
|
return ($type eq 'multipart') ? 'binary' : 'base64';
|
|
}
|
|
}
|
|
|
|
sub recommended_filename {
|
|
# --------------------------------------------------------------------------
|
|
# my $name = $obj->recommended_filename;
|
|
# --------------------------------------
|
|
# This method attempts to guess a filename for this part. The file name
|
|
# returned is checked for any illegal characters. Any found are stripted
|
|
# and what is left is returned. This method will return empty string
|
|
# on failure.
|
|
#
|
|
my $self = shift;
|
|
my $value;
|
|
|
|
# Start by trying to get 'filename' from the 'content-disposition':
|
|
$value = $self->mime_attr('content-disposition.filename');
|
|
return $self->_evil_file($value) if (defined($value) and $value ne '');
|
|
|
|
# No? Okay, try to get 'name' from the 'content-type':
|
|
$value = $self->mime_attr('content-type.name');
|
|
return $self->_evil_file($value) if (defined($value) and $value ne '');
|
|
return '';
|
|
}
|
|
|
|
sub body_as_string {
|
|
# --------------------------------------------------------------------------
|
|
# my $body = $obj->body_as_string;
|
|
# --------------------------------
|
|
# This method returns the entire body as a string. You should probably
|
|
# not be calling this unless you are sure the body is small enough to fit
|
|
# into memory.
|
|
#
|
|
my $self = shift;
|
|
my ($ret, $fh);
|
|
defined($self->{body_in}) or return;
|
|
if ($self->{body_in} eq 'FILE') {
|
|
$fh = $self->open('r');
|
|
}
|
|
elsif ($self->{body_in} eq 'HANDLE') {
|
|
$fh = $self->{io};
|
|
}
|
|
else {
|
|
return $self->{data};
|
|
}
|
|
local $/;
|
|
$ret = <$fh>;
|
|
seek($fh, 0, 0);
|
|
return $ret;
|
|
}
|
|
|
|
sub open {
|
|
# --------------------------------------------------------------------------
|
|
# my $fh = $obj->open($flag);
|
|
# ---------------------------
|
|
# If the mody is stored in a path this method opens it given the flag r or
|
|
# w. r is to open to read and w is to open to write. If you open to write
|
|
# the file is trucated. If the file is not stored in file this method will
|
|
# return undef.
|
|
#
|
|
my ($self, $mode) = @_;
|
|
my $io = \do { local *FH; *FH };
|
|
$self->{body_in} eq 'FILE'
|
|
or return $self->error('BADARGS', 'FATAL', 'The body must be stored in file for this method to be called');
|
|
my $path = $self->body_path;
|
|
$self->debug("Opening $path") if $self->{_debug};
|
|
if ($mode eq 'w') {
|
|
open $io, ">$path" or return $self->error("WRITEOPEN", "FATAL", $path, $!);
|
|
}
|
|
elsif ($mode eq 'r') {
|
|
open $io, "<$path" or return $self->error("READOPEN", "FATAL", $path, $!);
|
|
}
|
|
else {
|
|
return $self->error("BADARGS", "FATAL", '$obj->open ("r"); -or- $obj->open ("w");');
|
|
}
|
|
CORE::binmode($io);
|
|
return $io;
|
|
}
|
|
|
|
sub body_in {
|
|
# --------------------------------------------------------------------------
|
|
# my $in = $obj->body_in;
|
|
# -----------------------
|
|
# This method returns a flag that says where the body for this part is
|
|
# stored. It will be one of 4 thing. 'MEMORY', 'HANDLE', 'FILE' or undef.
|
|
# If this method returns undef it means this part does not have a body.
|
|
#
|
|
my $self = shift;
|
|
$self->{body_in} = shift if (@_ > 0);
|
|
return $self->{body_in};
|
|
}
|
|
|
|
sub body_data {
|
|
# --------------------------------------------------------------------------
|
|
# my $body = $obj->body_data;
|
|
# ---------------------------
|
|
# $obj->body_data($body);
|
|
# -----------------------
|
|
# This is a set or get method for the body if you are storing the body
|
|
# in memory or the body is stored in memory. This method sets the body_in
|
|
# flag to MEMORY if called with the body of the message.
|
|
#
|
|
my ($self, $data) = @_;
|
|
if (defined $data) {
|
|
$self->{body_in} = 'MEMORY';
|
|
if (ref $data) {
|
|
return $self->error('BADARGS', 'FATAL', "body_data: Body in memory can not be a referece");
|
|
}
|
|
$self->{data} = $data;
|
|
}
|
|
return $self->{data};
|
|
}
|
|
|
|
sub body_handle {
|
|
# --------------------------------------------------------------------------
|
|
# my $fh = $obj->body_handle;
|
|
# ---------------------------
|
|
# $obj->body_handle(\*FH);
|
|
# ------------------------
|
|
# This is a set or get method for the body handle. This sets the body_in
|
|
# flag to HANDLE if a file handle is passed in.
|
|
#
|
|
my ($self, $fh) = @_;
|
|
if ($fh and ref $fh eq "GLOB") {
|
|
$self->{io} = $fh;
|
|
$self->{body_in} = 'HANDLE';
|
|
}
|
|
return $self->{io};
|
|
}
|
|
|
|
sub body_path {
|
|
# --------------------------------------------------------------------------
|
|
# my $path = $obj->body_path;
|
|
# ---------------------------
|
|
# $obj->body_path('/path/to/file');
|
|
# ---------------------------------
|
|
# This is a set or get method for the body path. This sets the body_in
|
|
# flag to FILE if called to set the path.
|
|
#
|
|
my $self = shift;
|
|
my $path = shift;
|
|
if (defined $path) {
|
|
$self->{path} = $path;
|
|
$self->{body_in} = 'FILE';
|
|
}
|
|
return $self->{path};
|
|
}
|
|
|
|
|
|
#####################################################
|
|
## Private Methods ##
|
|
#####################################################
|
|
|
|
sub _insert {
|
|
# --------------------------------------------------------------------------
|
|
my ($self, $tag, $val) = @_;
|
|
return unless defined $val and defined $tag;
|
|
push @{$self->{header_lines}->{lc $tag}}, $val;
|
|
push @{$self->{header_order}}, [$tag, $#{$self->{header_lines}->{lc $tag}}];
|
|
}
|
|
|
|
sub _fmt_line {
|
|
# --------------------------------------------------------------------------
|
|
my ($self, $tag, $line) = @_;
|
|
|
|
($tag) = $line =~ /^($FIELD_NAME|From )/oi unless defined $tag;
|
|
|
|
defined($tag) and $tag =~ /^($FIELD_NAME|From )/oi or return $self->error("BADTAG", "WARN", $tag);
|
|
$tag =~ s/^([^ :]+):/$1/;
|
|
|
|
# Ensure the line starts with tag
|
|
($line =~ /[^ :]+\s*:\s?(.*)$/s) and $line = $1;
|
|
$line =~ s/\r?\n$//;
|
|
$line = decode_mimewords($line);
|
|
return ($tag, $line);
|
|
}
|
|
|
|
sub _tag_case {
|
|
# --------------------------------------------------------------------------
|
|
my ($self, $tag) = @_;
|
|
defined $tag or return $self->error("BADARGS", "FATAL", '$self->_tag_case ($tag)');
|
|
|
|
$tag =~ s/:$//;
|
|
$tag =~ s/^\s*//;
|
|
$tag =~ s/\s*$//;
|
|
|
|
# Change the case of the tag
|
|
# eq Message-Id
|
|
$tag =~ s/\b([a-z]+)/\u$1/g;
|
|
|
|
return $tag;
|
|
}
|
|
|
|
sub _decode_b {
|
|
# --------------------------------------------------------------------------
|
|
my $str = shift;
|
|
require GT::Mail::Parse;
|
|
return GT::Mail::Parse::decode_base64($str);
|
|
}
|
|
|
|
sub _decode_q {
|
|
# --------------------------------------------------------------------------
|
|
my $str = shift;
|
|
$str =~ s/=([\da-fA-F]{2})/pack("C", hex($1))/ge; # RFC-1522, Q rule 1
|
|
$str =~ s/_/\x20/g; # RFC-1522, Q rule 2
|
|
return $str;
|
|
}
|
|
|
|
sub _encode_q {
|
|
# --------------------------------------------------------------------------
|
|
my ($word) = @_;
|
|
$word =~ s/([_?=\x00-\x1F\x7F-\xFF ])/sprintf("=%02X", ord($1))/eog;
|
|
return $word;
|
|
}
|
|
|
|
sub _encode_b {
|
|
# --------------------------------------------------------------------------
|
|
my ($word) = @_;
|
|
my $ret;
|
|
local $GT::Mail::Encoder::CRLF = $CRLF;
|
|
GT::Mail::Encoder->gt_encode(encoding => 'base64', in => $word, out => sub { $ret .= $_[0] });
|
|
return $ret;
|
|
}
|
|
|
|
sub _evil_file {
|
|
# --------------------------------------------------------------------------
|
|
my $self = shift;
|
|
my $file = shift;
|
|
my $l;
|
|
$self->debug("$file being changed") if $self->{_debug};
|
|
|
|
# Windows case
|
|
if ($file =~ m|\\|) {
|
|
$file = substr($file, rindex($file,'\\') + 1);
|
|
}
|
|
|
|
# Unix case
|
|
elsif ($file =~ m|/|) {
|
|
$file = substr($file, rindex($file,'/') + 1);
|
|
}
|
|
|
|
# Mac case
|
|
elsif ($file =~ m|:|) {
|
|
$file = substr($file, rindex($file,':') + 1);
|
|
}
|
|
$file =~ s{[^a-z_A-Z0-9\-\.]+}{}g;
|
|
return length($file) < 40 ? $file : substr($file,0,40);
|
|
}
|
|
|
|
1;
|
|
|
|
__END__
|
|
|
|
=head1 NAME
|
|
|
|
GT::Mail::Parts - Data storage class for MIME parts
|
|
|
|
=head1 SYNOPSIS
|
|
|
|
use GT::Mail;
|
|
|
|
my $mail = new GT::Mail;
|
|
|
|
my $top_part = $mail->parse('/path/to/email');
|
|
|
|
# Access the emails as an array
|
|
my @to = $top_part->split_field('to');
|
|
my @from = $top_part->split_field('from');
|
|
|
|
# Access to the header fields
|
|
my $mailer = $top_part->get('X-Mailer');
|
|
my $subject = $top_part->get('Subject');
|
|
|
|
# Access to this parts sub part
|
|
if ($top_part->is_multipart) {
|
|
my @parts = $top_parts->parts;
|
|
for my $part (@parts) {
|
|
|
|
# Access parts of the header
|
|
print "Filename: ", $part->recommended_filename, "\n";
|
|
print "Part is multi-part\n" if $part->is_multipart;
|
|
|
|
# Get the body as a string
|
|
my $body = $part->body_as_string;
|
|
}
|
|
}
|
|
|
|
# Change who it is to
|
|
$top_part->set('to', 'scott@gossamer-threads.com');
|
|
|
|
# Remove the bcc line
|
|
$top_part->delete('bcc');
|
|
|
|
|
|
|
|
=head1 DESCRIPTION
|
|
|
|
GT::Mail::Parts is a class to provide methods to change and
|
|
access a MIME messages. The object for this class is meant to
|
|
be istansiated from L<GT::Mail>.
|
|
|
|
=head2 effective_type - Access the effective MIME type
|
|
|
|
my $type = $obj->effective_type;
|
|
|
|
if ($type eq 'application/octet-stream') {
|
|
...
|
|
}
|
|
|
|
This method returns the effective MIME Type of this objects part.
|
|
|
|
=head2 get - Access header tags.
|
|
|
|
my $subj = $obj->get('Subject');
|
|
|
|
# or if there is more than one
|
|
my @recv = $obj->get('Received');
|
|
|
|
Used to access any of the tags in the header of the MIME part. If the
|
|
tag requested is not present returns false. The first argument to this
|
|
method is the name of he tag you want to extract. This is case insensitive.
|
|
|
|
=head2 set - Set a header tag.
|
|
|
|
# Change who the email is to
|
|
$obj->set('to', 'scott@gossamer-threads.com');
|
|
|
|
# Change the second Received tag
|
|
$obj->set('Received', 'from unknown', 1);
|
|
|
|
Set any of the tags in the header. If the tag does not exist this will create
|
|
it. This method takes three arguments. The first is the name of the tag to
|
|
change or add, this is case insensitive. The second argument is the value for
|
|
the tag. The third zero based optional argument is the position. The position
|
|
will default to zero if it is not specified.
|
|
|
|
=head2 delete - Remove a header tag.
|
|
|
|
# Delete who the message is from
|
|
$obj->delete('from');
|
|
|
|
This method deletes the tag specified by the first argument from this MIME
|
|
part.
|
|
|
|
=head2 size - Access the total size.
|
|
|
|
my $size = $obj->size;
|
|
|
|
This method returns the total size of this part. This includes the header and
|
|
the body.
|
|
|
|
=head2 preamble - Set or get the preamble.
|
|
|
|
# Retrieve the preamble
|
|
my $pre = $obj->preamble;
|
|
|
|
# Set the preamble
|
|
$obj->preamble('This is a multi-part message in MIME format.');
|
|
|
|
This is a set get method for the preamble. The preamble is the part after the
|
|
head but before the first MIME boundary. This method makes no since if this
|
|
is not a multi-part part.
|
|
|
|
=head2 epilogue - Set or get the epilogue.
|
|
|
|
# Retrieve the epilogue
|
|
my $ep = $obj->epilogue;
|
|
|
|
# Set the epilogue
|
|
$obj->epilogue('This is my cool epilogue');
|
|
|
|
This is a set get method for the epilogue. The epilogue is the part of the
|
|
MIME part after the MIME boundary and before the next head. This method makes
|
|
no since if this is not a multi-part part.
|
|
|
|
=head2 mime_type - Set or get the MIME type.
|
|
|
|
my ($type, $subtype) = split('/', $obj->mime_type);
|
|
|
|
This method returns the MIME type of this part. You can pass in an argument
|
|
to change the MIME type as well. So you could do
|
|
|
|
$obj->mime_type('text/plain');
|
|
|
|
This is probably not such a good idea unless you are constructing the email from
|
|
scratch.
|
|
|
|
=head2 is_multipart - See if you have a multi-part part.
|
|
|
|
if ($obj->is_multipart) {
|
|
# do some multi-part stuff
|
|
}
|
|
|
|
Returns true is this part is a multi-part MIME part.
|
|
|
|
=head2 parts - Access sub parts.
|
|
|
|
my @parts = $obj->parts;
|
|
|
|
Returns the parts object this part contains. Returns false if this part does
|
|
no have any sub parts. The parts objects that returns are from this same class.
|
|
Any parts that are milti-part should contain parts.
|
|
|
|
=head2 multipart_boundary - Set or get the multi-part boundary.
|
|
|
|
my $boundary = $obj->multipart_boundary;
|
|
|
|
This returns the multi-part boundary for this part. Setting this is never needed
|
|
and may be removed in the future. This method only makes since if you are working
|
|
with a multi-part pert.
|
|
|
|
=head2 header_as_string - Access the whole header.
|
|
|
|
my $head = $obj->header_as_string;
|
|
|
|
This method creates and returns the header for this part. The returned header should
|
|
be fully rfc822 compliant. Avoid calling this method more than once, as it will build
|
|
the header from an internal data structure each time.
|
|
|
|
=head2 split_field - Retrieve the emails split up into an array.
|
|
|
|
my @to = $obj->split_field; # Defaults to 'to'
|
|
my @bcc = $obj->split_field('bcc');
|
|
|
|
This is mostly a utility method. It takes an option argument as to the field you want
|
|
the email address from (default is to), it then splits the emails on '\s*,\s*' that is
|
|
not inside quotes. Returns an array of the split up string.
|
|
|
|
=head2 suggest_encoding - Get a suggestion for encoding.
|
|
|
|
my $encode = $obj->suggest_encoding;
|
|
|
|
Returns a suggested encoding for the body of this message. This is useful to decide
|
|
what encoding you should use for the body when building an email. This is used in
|
|
L<GT::Mail::Parse> to decide how to encode the message body.
|
|
|
|
=head2 recommended_filename - Figure out the file name.
|
|
|
|
my $file = $obj->recommended_filename;
|
|
if ($file) {
|
|
...
|
|
}
|
|
|
|
This method tries to figure out the file name of this part. This does not make much
|
|
since if this part is not an attachment of some kind. Returns an empty string on
|
|
failure.
|
|
|
|
=head2 body_as_string - Get the body as a string.
|
|
|
|
This method returns the entire body of the MIME message as a string. You should not
|
|
use this method if the body could be large.
|
|
|
|
=head2 body_in - Find the body.
|
|
|
|
my $in = $obj->body_in;
|
|
my $body;
|
|
|
|
if ($in eq 'MEMORY') {
|
|
$body = $obj->body_data;
|
|
}
|
|
elsif ($in eq 'HANDLE') {
|
|
$body = $obj->body_handle;
|
|
}
|
|
elsif ($in eq 'FILE') {
|
|
$body = $obj->body_path;
|
|
}
|
|
|
|
This method returns the location of the body. The location can be one of three things:
|
|
|
|
=over 4
|
|
|
|
=item MEMORY
|
|
|
|
The body is in a string.
|
|
|
|
=item HANDLE
|
|
|
|
The body is in an IO handle.
|
|
|
|
=item FILE
|
|
|
|
The body is in a file.
|
|
|
|
=back
|
|
|
|
You would use this to decide what method to use to access the body. If the MIME message
|
|
was parsed into GT::Mail::Parts using L<GT::Mail::Parser> the body will always be in
|
|
a FILE.
|
|
|
|
|
|
=head2 body_data - Get the in memory body.
|
|
|
|
This method returns the body if it is stored in memory. Returns undefined if the body is
|
|
not in memory.
|
|
|
|
=head2 body_handle - Get an IO handle to the body.
|
|
|
|
This method returns a handle to the body if the body is stored in a handle for this part.
|
|
Returns undefined if not.
|
|
|
|
=head2 body_path - Get the location of the file the body is in.
|
|
|
|
This method returns the file path to the file the body is located in if the body for this
|
|
part is stored in a file. Returns undefined if not.
|
|
|
|
=head1 COPYRIGHT
|
|
|
|
Copyright (c) 2004 Gossamer Threads Inc. All Rights Reserved.
|
|
http://www.gossamer-threads.com/
|
|
|
|
=head1 VERSION
|
|
|
|
$Revision: 1.84 $
|
|
|
|
=cut
|