package MIME::Lite;

require 5.004;     ### for /c modifier in m/\G.../gc modifier

use Carp;
use FileHandle;

use strict;
use vars qw(
            $AUTO_CC
            $AUTO_ENCODE
            $AUTO_VERIFY
            $PARANOID
            $QUIET
            $VANILLA
            $VERSION
            );

# GLOBALS, EXTERNAL/CONFIGURATION...

### The package version, both in 1.23 style *and* usable by MakeMaker:
$VERSION = substr q$Revision: 2.106 $, 10;

### Don't warn me about dangerous activities:
$QUIET = "true";

### Set this true if you don't want to use MIME::Base64/MIME::QuotedPrint:
$PARANOID = "true";

### Unsupported (for tester use): don't qualify boundary with time/pid:
$VANILLA = 0;

### Automatically choose encoding from content type:
$AUTO_ENCODE = 1;

### Automatically interpret CC/BCC for SMTP:
$AUTO_CC = 1;

### Check paths right before printing:
$AUTO_VERIFY = 1;

# GLOBALS, INTERNAL...

### Our sending facilities:
my $Sender     = "sendmail";
my %SenderArgs = (
    "sendmail" => ["/usr/lib/sendmail -t -oi -oem"],
    "smtp"     => [],
    "sub"      => [],
);

### Boundary counter:
my $BCount = 0;

### Known Mail/MIME fields... these, plus some general forms like
### "x-*", are recognized by build():
my %KnownField = map {$_=>1}
qw(
   bcc         cc          comments      date          encrypted
   from        keywords    message-id    mime-version  organization
   received    references  reply-to      return-path   sender
   subject     to
   );

### What external packages do we use for encoding?
my @Uses;


# PRIVATE UTILITY FUNCTIONS...

sub fold {
    my $str = shift;
    $str =~ s/^\s*|\s*$//g;    ### trim
    $str =~ s/\n/\n /g;
    $str;
}


sub gen_boundary {
    return ("_----------=_".($VANILLA ? '' : int(time).$$).$BCount++);
}


sub known_field {
    my $field = lc(shift);
    $KnownField{$field} or ($field =~ m{^(content|resent|x)-.});
}


sub is_mime_field {
    $_[0] =~ /^(mime\-|content\-)/i;
}

my $ATOM      = '[^ \000-\037()<>@,;:\134"\056\133\135]+';
my $QSTR      = '".*?"';
my $WORD      = '(?:' . $QSTR . '|' . $ATOM . ')';
my $DOMAIN    = '(?:' . $ATOM . '(?:' . '\\.' . $ATOM . ')*' . ')';
my $LOCALPART = '(?:' . $WORD . '(?:' . '\\.' . $WORD . ')*' . ')';
my $ADDR      = '(?:' . $LOCALPART . '@' . $DOMAIN . ')';
my $PHRASE    = '(?:' . $WORD . ')+';
my $SEP       = "(?:^\\s*|\\s*,\\s*)";     ### before elems in a list

sub my_extract_addrs {
    my $str = shift;
    my @addrs;
    $str =~ s/\s/ /g;     ### collapse whitespace

    pos($str) = 0;
    while ($str !~ m{\G\s*\Z}gco) {
	### print STDERR "TACKLING: ".substr($str, pos($str))."\n";
	if    ($str =~ m{\G$SEP$PHRASE\s*<\s*($ADDR)\s*>}gco) {push @addrs,$1}
	elsif ($str =~ m{\G$SEP($ADDR)}gco)                   {push @addrs,$1}
	elsif ($str =~ m{\G$SEP($ATOM)}gco)                   {push @addrs,$1}
	else {
	    my $problem = substr($str, pos($str));
	    die "can't extract address at <$problem> in <$str>\n";
	}
    }
    return @addrs;
}


    eval q{
        sub extract_addrs {
	    return my_extract_addrs(@_);
	}
    }; ### q


if (!$PARANOID and eval "require MIME::Base64") {
    import MIME::Base64 qw(encode_base64);
    push @Uses, "B$MIME::Base64::VERSION";
}
else {
    eval q{
sub encode_base64 {
    my $res = "";
    my $eol = "\n";

    pos($_[0]) = 0;        ### thanks, Andreas!
    while ($_[0] =~ /(.{1,45})/gs) {
	$res .= substr(pack('u', $1), 1);
	chop($res);
    }
    $res =~ tr|` -_|AA-Za-z0-9+/|;

    ### Fix padding at the end:
    my $padding = (3 - length($_[0]) % 3) % 3;
    $res =~ s/.{$padding}$/'=' x $padding/e if $padding;

    ### Break encoded string into lines of no more than 76 characters each:
    $res =~ s/(.{1,76})/$1$eol/g if (length $eol);
    return $res;
} ### sub
  } ### q
} ### if


if (!$PARANOID and eval "require MIME::QuotedPrint") {
    import MIME::QuotedPrint qw(encode_qp);
    push @Uses, "Q$MIME::QuotedPrint::VERSION";
}
else {
    eval q{
sub encode_qp {
    my $res = shift;
    local($_);
    $res =~ s/([^ \t\n!-<>-~])/sprintf("=%02X", ord($1))/eg;  ### rule #2,#3
    $res =~ s/([ \t]+)$/
      join('', map { sprintf("=%02X", ord($_)) }
	           split('', $1)
      )/egm;                        ### rule #3 (encode whitespace at eol)

    ### rule #5 (lines shorter than 76 chars, but can't break =XX escapes:
    my $brokenlines = "";
    $brokenlines .= "$1=\n" while $res =~ s/^(.{70}([^=]{2})?)//; ### 70 was 74
    $brokenlines =~ s/=\n$// unless length $res;
    "$brokenlines$res";
} ### sub
  } ### q
} ### if


#------------------------------
#
# encode_8bit STRING
#
# Encode the given string using 8BIT.
# This breaks long lines into shorter ones.

sub encode_8bit {
    my $str = shift;
    $str =~ s/^(.{990})/$1\n/mg;
    $str;
}

#------------------------------
#
# encode_7bit STRING
#
# Encode the given string using 7BIT.
# This NO LONGER protects people through encoding.

sub encode_7bit {
    my $str = shift;
    $str =~ s/[\x80-\xFF]//g;
    $str =~ s/^(.{990})/$1\n/mg;
    $str;
}




sub new {
    my $class = shift;

    ### Create basic object:
    my $self = {
	Attrs   => {},     ### MIME attributes
	Header  => [],     ### explicit message headers
	Parts   => [],     ### array of parts
    };
    bless $self, $class;

    ### Build, if needed:
    return (@_ ? $self->build(@_) : $self);
}



sub attach {
    my $self = shift;

    ### Create new part, if necessary:
    my $part1 = ((@_ == 1) ? shift : ref($self)->new(Top=>0, @_));

    ### Do the "attach-to-singlepart" hack:
    if ($self->attr('content-type') !~ m{^(multipart|message)/}i) {

	### Create part zero:
	my $part0 = ref($self)->new;

	### Cut MIME stuff from self, and paste into part zero:
	foreach (qw(Attrs Data Path FH)) {
	    $part0->{$_} = $self->{$_}; delete($self->{$_});
	}
	$part0->top_level(0);    ### clear top-level attributes

	### Make self a top-level multipart:
	$self->{Attrs} ||= {};   ### reset
	$self->attr('content-type'              => 'multipart/mixed');
	$self->attr('content-type.boundary'     => gen_boundary());
	$self->attr('content-transfer-encoding' => '7bit');
	$self->top_level(1);     ### activate top-level attributes

	### Add part 0:
	push @{$self->{Parts}}, $part0;
    }

    ### Add the new part:
    push @{$self->{Parts}}, $part1;
    $part1;
}


sub build {
    my $self = shift;
    my %params = @_;
    my @params = @_;
    my $key;

    ### Miko's note: reorganized to check for exactly one of Data, Path, or FH
    (defined($params{Data})+defined($params{Path})+defined($params{FH}) <= 1)
	or croak "supply exactly zero or one of (Data|Path|FH).\n";

    ### Create new instance, if necessary:
    ref($self) or $self = $self->new;


    ### CONTENT-TYPE....
    ###

    ### Get content-type:
    my $type = ($params{Type} || 'TEXT');
    ($type eq 'TEXT')   and $type = 'text/plain';
    ($type eq 'BINARY') and $type = 'application/octet-stream';
    $type = lc($type);
    $self->attr('content-type' => $type);

    ### Get some basic attributes from the content type:
    my $is_multipart = ($type =~ m{^(multipart)/}i);

    ### Add in the multipart boundary:
    if ($is_multipart) {
	my $boundary = gen_boundary();
	$self->attr('content-type.boundary' => $boundary);
    }


    ### CONTENT-ID...
    ###
    $self->attr('content-id' => $params{Id}) if defined($params{Id});


    ### DATA OR PATH...
    ###    Note that we must do this *after* we get the content type,
    ###    in case read_now() is invoked, since it needs the binmode().

    ### Get data, as...
    ### ...either literal data:
    if (defined($params{Data})) {
	$self->data($params{Data});
    }
    ### ...or a path to data:
    elsif (defined($params{Path})) {
	$self->path($params{Path});       ### also sets filename
	$self->read_now if $params{ReadNow};
    }
    ### ...or a filehandle to data:
    ### Miko's note: this part works much like the path routine just above,
    elsif (defined($params{FH})) {
	$self->fh($params{FH});
	$self->read_now if $params{ReadNow};  ### implement later
    }


    ### FILENAME... (added by Ian Smith <ian@safeway.dircon.co.uk> on 8/4/97)
    ###    Need this to make sure the filename is added.  The Filename
    ###    attribute is ignored, otherwise.
    if (defined($params{Filename})) {
	$self->filename($params{Filename});
    }


    ### CONTENT-TRANSFER-ENCODING...
    ###

    ### Get it:
    my $enc = ($params{Encoding} ||
	       ($AUTO_ENCODE and $self->suggest_encoding($type)) ||
	       'binary');
    $self->attr('content-transfer-encoding' => lc($enc));

    ### Sanity check:
    if ($type =~ m{^(multipart|message)/}) {
	($enc =~ m{^(7bit|8bit|binary)\Z}) or
	    croak "illegal MIME: can't have encoding $enc with type $type\n";
    }

    ### CONTENT-DISPOSITION...
    ###    Default is inline for single, none for multis:
    ###
    my $disp = ($params{Disposition} or ($is_multipart ? undef : 'inline'));
    $self->attr('content-disposition' => $disp);

    ### CONTENT-LENGTH...
    ###
    my $length;
    if (exists($params{Length})) {   ### given by caller:
	$self->attr('content-length' => $params{Length});
    }
    else {                           ### compute it ourselves
	$self->get_length;
    }

    ### Init the top-level fields:
    my $is_top = defined($params{Top}) ? $params{Top} : 1;
    $self->top_level($is_top);

    ### Datestamp if desired:
    my $ds_wanted    = $params{Datestamp};
    my $ds_defaulted = ($is_top and !exists($params{Datestamp}));
    if (($ds_wanted or $ds_defaulted) and !exists($params{Date})) {
	my ($u_wdy, $u_mon, $u_mdy, $u_time, $u_y4) =
	    split /\s+/, gmtime()."";   ### should be non-locale-dependent
	my $date = "$u_wdy, $u_mdy $u_mon $u_y4 $u_time UT";
	$self->add("date", $date);
    }

    ### Set message headers:
    my @paramz = @params;
    my $field;
    while (@paramz) {
	my ($tag, $value) = (shift(@paramz), shift(@paramz));

	### Get tag, if a tag:
	if ($tag =~ /^-(.*)/) {      ### old style, backwards-compatibility
	    $field = lc($1);
	}
	elsif ($tag =~ /^(.*):$/) {  ### new style
	    $field = lc($1);
	}
	elsif (known_field($field = lc($tag))) {   ### known field
	    ### no-op
	}
	else {                       ### not a field:
	    next;
	}

	### Add it:
	$self->add($field, $value);
    }

    ### Done!
    $self;
}



sub top_level {
    my ($self, $onoff) = @_;
    if ($onoff) {
	$self->attr('MIME-Version' => '1.0');
	my $uses = (@Uses ? ("(" . join("; ", @Uses) . ")") : '');
	$self->replace('X-Mailer' => "MIME::Lite $VERSION $uses")
	    unless $VANILLA;
    }
    else {
	$self->attr('MIME-Version' => undef);
	$self->delete('X-Mailer');
    }
}


sub add {
    my $self = shift;
    my $tag = lc(shift);
    my $value = shift;

    ### If a dangerous option, warn them:
    carp "Explicitly setting a MIME header field ($tag) is dangerous:\n".
	 "use the attr() method instead.\n"
	if (is_mime_field($tag) && !$QUIET);

    ### Get array of clean values:
    my @vals = ref($value) ? @{$value} : ($value);
    map { s/\n/\n /g } @vals;

    ### Add them:
    foreach (@vals) {
	push @{$self->{Header}}, [$tag, $_];
    }
}


sub attr {
    my ($self, $attr, $value) = @_;
    $attr = lc($attr);

    ### Break attribute name up:
    my ($tag, $subtag) = split /\./, $attr;
    defined($subtag) or $subtag = '';

    ### Set or get?
    if (@_ > 2) {   ### set:
	$self->{Attrs}{$tag} ||= {};            ### force hash
	delete $self->{Attrs}{$tag}{$subtag};   ### delete first
	if (defined($value)) {                  ### set...
	    $value =~ s/[\r\n]//g;                   ### make clean
	    $self->{Attrs}{$tag}{$subtag} = $value;
	}
    }

    ### Return current value:
    $self->{Attrs}{$tag}{$subtag};
}

sub _safe_attr {
    my ($self, $attr) = @_;
    my $v = $self->attr($attr);
    defined($v) ? $v : '';
}


sub delete {
    my $self = shift;
    my $tag = lc(shift);

    ### Delete from the header:
    my $hdr = [];
    my $field;
    foreach $field (@{$self->{Header}}) {
	push @$hdr, $field if ($field->[0] ne $tag);
    }
    $self->{Header} = $hdr;
    $self;
}


sub fields {
    my $self = shift;
    my @fields;

    ### Get a lookup-hash of all *explicitly-given* fields:
    my %explicit = map { $_->[0] => 1 } @{$self->{Header}};

    ### Start with any MIME attributes not given explicitly:
    my $tag;
    foreach $tag (sort keys %{$self->{Attrs}}) {

	### Skip if explicit:
	next if ($explicit{$tag});

	### Skip if no subtags:
	my @subtags = keys %{$self->{Attrs}{$tag}};
	@subtags or next;

	### Create string:
	my $value;
	defined($value = $self->{Attrs}{$tag}{''}) or next;  ### need default
	foreach (sort @subtags) {
	    next if ($_ eq '');
	    $value .= qq{; $_="$self->{Attrs}{$tag}{$_}"};
	}

	### Add to running fields;
	push @fields, [$tag, $value];
    }

    ### Add remaining fields (note that we duplicate the array for safety):
    foreach (@{$self->{Header}}) {
	push @fields, [@{$_}];
    }

    ### Done!
    return \@fields;
}

sub filename {
    my ($self, $filename) = @_;
    if (@_ > 1) {
	$self->attr('content-type.name' => $filename);
	$self->attr('content-disposition.filename' => $filename);
    }
    $self->attr('content-disposition.filename');
}


sub get {
    my ($self, $tag, $index) = @_;
    $tag = lc($tag);
    croak "get: can't be used with MIME fields\n" if is_mime_field($tag);

    my @all = map { ($_->[0] eq $tag) ? $_->[1] : ()} @{$self->{Header}};
    (defined($index) ? $all[$index] : (wantarray ? @all : $all[0]));
}

#------------------------------


sub get_length {
    my $self = shift;

    my $is_multipart = ($self->attr('content-type') =~ m{^multipart/}i);
    my $enc = lc($self->attr('content-transfer-encoding') || 'binary');
    my $length;
    if (!$is_multipart && ($enc eq "binary")){  ### might figure it out cheap:
	if    (defined($self->{Data})) {               ### it's in core
	    $length = length($self->{Data});
	}
	elsif (defined($self->{FH})) {                 ### it's in a filehandle
	    ### no-op: it's expensive, so don't bother
	}
	elsif (defined($self->{Path})) {               ### it's a simple file!
	    $length = (-s $self->{Path})   if (-e $self->{Path});
	}
    }
    $self->attr('content-length' => $length);
    return $length;
}


sub replace {
    my ($self, $tag, $value) = @_;
    $self->delete($tag);
    $self->add($tag, $value) if defined($value);
}



sub scrub {
    my ($self, @a) = @_;
    my ($expl) = @a;
    local $QUIET = 1;

    ### Scrub me:
    if (!@a) {         ### guess

	### Scrub length always:
	$self->replace('content-length', '');

	### Scrub disposition if no filename, or if content-type has same info:
	if (!$self->_safe_attr('content-disposition.filename') ||
	    $self->_safe_attr('content-type.name')) {
	    $self->replace('content-disposition', '');
	}

	### Scrub encoding if effectively unencoded:
	if ($self->_safe_attr('content-transfer-encoding') =~
	    /^(7bit|8bit|binary)$/i) {
	    $self->replace('content-transfer-encoding', '');
	}

	### Scrub charset if US-ASCII:
	if ($self->_safe_attr('content-type.charset') =~ /^(us-ascii)/i) {
	    $self->attr('content-type.charset' => undef);
	}

	### TBD: this is not really right for message/digest:
	if ((keys %{$self->{Attrs}{'content-type'}} == 1) and
	    ($self->_safe_attr('content-type') eq 'text/plain')) {
	    $self->replace('content-type', '');
	}
    }
    elsif ($expl and (ref($expl) eq 'ARRAY')) {
	foreach (@{$expl}) { $self->replace($_, ''); }
    }

    ### Scrub my kids:
    foreach (@{$self->{Parts}}) { $_->scrub(@a); }
}


sub binmode {
    my $self = shift;
    $self->{Binmode} = shift if (@_);       ### argument? set override
    return (defined($self->{Binmode})
	    ? $self->{Binmode}
	    : ($self->attr("content-type") !~ m{^(text|message)/}i));
}


sub data {
    my $self = shift;
    if (@_) {
	$self->{Data} = ((ref($_[0]) eq 'ARRAY') ? join('', @{$_[0]}) : $_[0]);
	$self->get_length;
    }
    $self->{Data};
}



sub path {
    my $self = shift;
    if (@_) {

	### Set the path, and invalidate the content length:
	$self->{Path} = shift;

	### Re-set filename, extracting it from path if possible:
	my $filename;
	if ($self->{Path} and ($self->{Path} !~ /\|$/)) {  ### non-shell path:
	    ($filename = $self->{Path}) =~ s/^<//;
	    ($filename) = ($filename =~ m{([^\/]+)\Z});
	}
	$self->filename($filename);

	### Reset the length:
	$self->get_length;
    }
    $self->{Path};
}


sub fh {
    my $self = shift;
    $self->{FH} = shift if @_;
    $self->{FH};
}


sub resetfh {
    my $self = shift;
    seek($self->{FH},0,0);
}


sub read_now {
    my $self = shift;
    local $/ = undef;

    if    ($self->{FH}) {       ### data from a filehandle:
	my $chunk;
	my @chunks;
	CORE::binmode($self->{FH}) if $self->binmode;
	while (read($self->{FH}, $chunk, 1024)) {
	    push @chunks, $chunk;
	}
	$self->{Data} = join '', @chunks;
    }
    elsif ($self->{Path}) {     ### data from a path:
	open SLURP, $self->{Path} or croak "open $self->{Path}: $!\n";
	CORE::binmode(SLURP) if $self->binmode;
	$self->{Data} = <SLURP>;        ### sssssssssssssslurp...
	close SLURP;                    ### ...aaaaaaaaahhh!
    }
}


sub sign {
    my $self = shift;
    my %params = @_;

    ### Default:
    @_ or $params{Path} = "$ENV{HOME}/.signature";

    ### Force message in-core:
    defined($self->{Data}) or $self->read_now;

    ### Load signature:
    my $sig;
    if (!defined($sig = $params{Data})) {      ### not given explicitly:
	local $/ = undef;
	open SIG, $params{Path} or croak "open sig $params{Path}: $!\n";
	$sig = <SIG>;                  ### sssssssssssssslurp...
	close SIG;                     ### ...aaaaaaaaahhh!
    }
    $sig = join('',@$sig) if (ref($sig) and (ref($sig) eq 'ARRAY'));

    ### Append, following Internet conventions:
    $self->{Data} .= "\n-- \n$sig";

    ### Re-compute length:
    $self->get_length;
    1;
}


sub suggest_encoding {
    my ($self, $ctype) = @_;

    my ($type) = split '/', lc($ctype);
    if (($type eq 'text') || ($type eq 'message')) {    ### scan message body
	return 'binary';
    }
    else {
	return ($type eq 'multipart') ? 'binary' : 'base64';
    }
}


sub verify_data {
    my $self = shift;

    ### Verify self:
    my $path = $self->{Path};
    if ($path and ($path !~ /\|$/)) {  ### non-shell path:
	$path =~ s/^<//;
	(-r $path) or die "$path: not readable\n";
    }

    ### Verify parts:
    foreach my $part (@{$self->{Parts}}) { $part->verify_data }
    1;
}


sub print {
    my ($self, $out) = @_;

    ### Coerce into a printable output handle:
    $out = wrap MIME::Lite::IO_Handle $out;

    ### Output head, separator, and body:
    $out->print($self->header_as_string, "\n");
    $self->print_body($out);
}

sub print_for_smtp {
    my ($self, $out) = @_;

    ### Coerce into a printable output handle:
    $out = wrap MIME::Lite::IO_Handle $out;

    ### Create a safe head:
    my @fields = grep { $_->[0] ne 'bcc' } @{$self->fields};
    my $header = $self->fields_as_string(\@fields);

    ### Output head, separator, and body:
    $out->print($header, "\n");
    $self->print_body($out);
}


sub print_body {
    my ($self, $out) = @_;

    ### Coerce into a printable output handle:
    $out = wrap MIME::Lite::IO_Handle $out;

    ### Output either the body or the parts.
    ###   Notice that we key off of the content-type!  We expect fewer
    ###   accidents that way, since the syntax will always match the MIME type.
    my $type = $self->attr('content-type');
    if ($type =~ m{^multipart/}i) {
	my $boundary = $self->attr('content-type.boundary');

	### Preamble:
	$out->print("This is a multi-part message in MIME format.\n");

	### Parts:
	my $part;
	foreach $part (@{$self->{Parts}}) {
	    $out->print("\n--$boundary\n");
	    $part->print($out);
	}

	### Epilogue:
	$out->print("\n--$boundary--\n\n");
    }
    elsif ($type =~ m{^message/}) {
	my @parts = @{$self->{Parts}};

	### It's a toss-up; try both data and parts:
	if    (@parts == 0) { $self->print_simple_body($out) }
	elsif (@parts == 1) { $parts[0]->print($out) }
	else                { croak "can't handle message with >1 part\n"; }
    }
    else {
	$self->print_simple_body($out);
    }
    1;
}

sub print_simple_body {
    my ($self, $out) = @_;

    ### Coerce into a printable output handle:
    $out = wrap MIME::Lite::IO_Handle $out;

    ### Get content-transfer-encoding:
    my $encoding = uc($self->attr('content-transfer-encoding'));

    ### Notice that we don't just attempt to slurp the data in from a file:
    ### by processing files piecemeal, we still enable ourselves to prepare
    ### very large MIME messages...

    ### Is the data in-core?  If so, blit it out...
    if (defined($self->{Data})) {
      DATA:
	{ $_ = $encoding;

	  /^BINARY$/ and do {
	      $out->print($self->{Data});
	      last DATA;
	  };
	  /^8BIT$/ and do {
	      $out->print(encode_8bit($self->{Data}));
	      last DATA;
	  };
	  /^7BIT$/ and do {
	      $out->print(encode_7bit($self->{Data}));
	      last DATA;
	  };
	  /^QUOTED-PRINTABLE$/ and do {
	      while ($self->{Data}=~ m{^(.*[\r\n]*)}mg) {
		  $out->print(encode_qp($1)); ### have to do it line by line...
	      }
	      last DATA;
	  };
	  /^BASE64/ and do {
	      $out->print(encode_base64($self->{Data}));
	      last DATA;
	  };
	  croak "unsupported encoding: `$_'\n";
        }
    }

    ### Else, is the data in a file?  If so, output piecemeal...
    ###    Miko's note: this routine pretty much works the same with a path
    ###    or a filehandle. the only difference in behaviour is that it does
    ###    not attempt to open anything if it already has a filehandle
    elsif (defined($self->{Path}) || defined($self->{FH})) {
	no strict 'refs';          ### in case FH is not an object
	my $DATA;

	### Open file if necessary:
	if (defined($self->{Path})) {
	    $DATA = new FileHandle || croak "can't get new filehandle\n";
	    $DATA->open("$self->{Path}") or croak "open $self->{Path}: $!\n";
	}
	else {
	    $DATA=$self->{FH};
	}
	CORE::binmode($DATA) if $self->binmode;

	### Encode piece by piece:
      PATH:
	{   $_ = $encoding;

	    /^BINARY$/ and do {
		$out->print($_)                while read($DATA, $_, 2048);
		last PATH;
	    };
	    /^8BIT$/ and do {
		$out->print(encode_8bit($_))   while (<$DATA>);
		last PATH;
	    };
	    /^7BIT$/ and do {
		$out->print(encode_7bit($_))   while (<$DATA>);
		last PATH;
	    };
	    /^QUOTED-PRINTABLE$/ and do {
		$out->print(encode_qp($_))     while (<$DATA>);
		last PATH;
	    };
	    /^BASE64$/ and do {
		$out->print(encode_base64($_)) while (read($DATA, $_, 45));
		last PATH;
	    };
	    croak "unsupported encoding: `$_'\n";
	}

	### Close file:
	close $DATA if defined($self->{Path});
    }

    else {
	croak "no data in this part\n";
    }
    1;
}


sub print_header {
    my ($self, $out) = @_;

    ### Coerce into a printable output handle:
    $out = wrap MIME::Lite::IO_Handle $out;

    ### Output the header:
    $out->print($self->header_as_string);
    1;
}


sub body_as_string {
    my $self = shift;
    my @buf;
    my $io = (wrap MIME::Lite::IO_ScalarArray \@buf);
    $self->print_body($io);
    join '', @buf;
}
*stringify_body = \&body_as_string;    ### backwards compatibility

sub fields_as_string {
    my ($self, $fields) = @_;
    my @lines;
    foreach (@$fields) {
	my ($tag, $value) = @$_;
	next if ($value eq '');          ### skip empties
	$tag =~ s/\b([a-z])/uc($1)/ge;   ### make pretty
	$tag =~ s/^mime-/MIME-/ig;       ### even prettier
	push @lines, "$tag: $value\n";
    }
    join '', @lines;
}


sub header_as_string {
    my $self = shift;
    $self->fields_as_string($self->fields);
}
*stringify_header = \&header_as_string;    ### backwards compatibility


sub send {
    my $self = shift;

    if (ref($self)) {              ### instance method:
	my $method = "send_by_$Sender";
	my @args   = @{$SenderArgs{$Sender} || []};
	$self->verify_data if $AUTO_VERIFY;       ### prevents missing parts!
	return $self->$method(@args);
    }
    else {                         ### class method:
	my @old = ($Sender, @{$SenderArgs{$Sender}});
	$Sender = shift;
	$SenderArgs{$Sender} = [@_];    ### remaining args
	return @old;
    }
}


sub send_by_sendmail {
    my $self = shift;

    if (@_ == 1) {                    ### Use the given command...
	my $sendmailcmd = shift @_;

	### Do it:
	open SENDMAIL, "|$sendmailcmd" or croak "open |$sendmailcmd: $!\n";
	$self->print(\*SENDMAIL);
	close SENDMAIL;
	return (($? >> 8) ? undef : 1);
    }
    else {                            ### Build the command...
	my %p = @_;
	$p{Sendmail} ||= "/usr/lib/sendmail";

	### Start with the command and basic args:
	my @cmd = ($p{Sendmail}, @{$p{BaseArgs} || ['-t', '-oi', '-oem']});

	### See if we are forcibly setting the sender:
	$p{SetSender} = 1 if defined($p{FromSender});

	### Add the -f argument, unless we're explicitly told NOT to:
	unless (exists($p{SetSender}) and !$p{SetSender}) {
	    my $from = $p{FromSender} || ($self->get('From'))[0];
	    if ($from) {
		my ($from_addr) = extract_addrs($from);
		push @cmd, "-f$from_addr"       if $from_addr;
	    }
	}

	### Open the command in a taint-safe fashion:
	my $pid = open SENDMAIL, "|-";
	defined($pid) or die "open of pipe failed: $!\n";
	if (!$pid) {    ### child
	    exec(@cmd) or die "can't exec $p{Sendmail}: $!\n";
	    ### NOTREACHED
	}
	else {          ### parent
	    $self->print(\*SENDMAIL);
	    close SENDMAIL || die "error closing $p{Sendmail}: $! (exit $?)\n";
	    return 1;
	}
    }
}


### Provided by Andrew McRae. Version 0.2  anm  09Sep97
### Copyright 1997 Optimation New Zealand Ltd.
### May be modified/redistributed under the same terms as Perl.
#
sub send_by_smtp {
    my ($self, @args) = @_;

    ### We need the "From:" and "To:" headers to pass to the SMTP mailer:
    my $hdr  = $self->fields();
    my $from = $self->get('From');
    my $to   = $self->get('To');

    ### Sanity check:
    defined($to) or croak "send_by_smtp: missing 'To:' address\n";

    ### Get the destinations as a simple array of addresses:
    my @to_all = extract_addrs($to);
    if ($AUTO_CC) {
	foreach my $field (qw(Cc Bcc)) {
	    my $value = $self->get($field);
	    push @to_all, extract_addrs($value) if defined($value);
	}
    }

    ### Create SMTP client:
    require Net::SMTP;

	$! = "";
    my $smtp = MIME::Lite::SMTP->new(@args)
        or die
	"Unable to connect to the SMTP server.  The following error was returned: $!\n\nThe server either does not exist, or rejected the connection.\n\nPlease ensure that the UBB is allowed to relay mail through the server.\n";
    $smtp->mail($from)
        or die
	"The SMTP server returned the following message: \n$!\n" . $smtp->code() . $smtp->message() . "\n" .
	"The SMTP server has rejected the email address the UBB tried to send mail as ($from).\n  Please ensure that this is a valid email address for this domain name.\n";
    $smtp->to(@to_all)
        or die
	"The SMTP server returned the following message: \n$!\n" . $smtp->code() . $smtp->message() . "\n" .
	"The SMTP server has rejected the email address the UBB tried to send mail to.\nPlease ensure that the UBB is allowed to relay mail through the SMTP server,\nand that the From: email address ($from) is a valid email address for this domain.\n";
    $smtp->data()
        or die
	"The SMTP server returned the following message: \n$!\n" . $smtp->code() . $smtp->message() . "\n" .
	"The SMTP server has rejected the data that the UBB tried to send in a mail.\nPlease ensure that the UBB is allowed to relay mail through the SMTP server,\nand that the From: email address ($from) is a valid email address for this domain.\n";

    ### MIME::Lite can print() to anything with a print() method:
    $self->print_for_smtp($smtp);
    $smtp->dataend();
    $smtp->quit;
    1;
}

#------------------------------
#
# send_by_sub [\&SUBREF, [ARGS...]]
#
# I<Instance method, private.>
# Send the message via an anonymous subroutine.
#
sub send_by_sub {
    my ($self, $subref, @args) = @_;
    &$subref($self, @args);
}


sub sendmail {
    my $self = shift;
    $self->send('sendmail', join(' ', @_));
}


sub quiet {
    my $class = shift;
    $QUIET = shift if @_;
    $QUIET;
}



#============================================================

package MIME::Lite::SMTP;

#============================================================
# This class just adds a print() method to Net::SMTP.
# Notice that we don't use/require it until it's needed!

use strict;
use vars qw( @ISA );
@ISA = qw(Net::SMTP);

sub print { shift->datasend(@_) }



#============================================================

package MIME::Lite::IO_Handle;

#============================================================

### Wrap a non-object filehandle inside a blessed, printable interface:
### Does nothing if the given $fh is already a blessed object.
sub wrap {
    my ($class, $fh) = @_;
    no strict 'refs';

    ### Get default, if necessary:
    $fh or $fh = select;        ### no filehandle means selected one
    ref($fh) or $fh = \*$fh;    ### scalar becomes a globref

    ### Stop right away if already a printable object:
    return $fh if (ref($fh) and (ref($fh) ne 'GLOB'));

    ### Get and return a printable interface:
    bless \$fh, $class;         ### wrap it in a printable interface
}

### Print:
sub print {
    my $self = shift;
    print {$$self} @_;
}


#============================================================

package MIME::Lite::IO_Scalar;

#============================================================

### Wrap a scalar inside a blessed, printable interface:
sub wrap {
    my ($class, $scalarref) = @_;
    defined($scalarref) or $scalarref = \"";
    bless $scalarref, $class;
}

### Print:
sub print {
    my $self = shift;
    $$self .= join('', @_);
    1;
}


#============================================================

package MIME::Lite::IO_ScalarArray;

#============================================================

### Wrap an array inside a blessed, printable interface:
sub wrap {
    my ($class, $arrayref) = @_;
    defined($arrayref) or $arrayref = [];
    bless $arrayref, $class;
}

### Print:
sub print {
    my $self = shift;
    push @$self, @_;
    1;
}

1;
__END__

#
#
#Copyright (c) 1997 by Eryq.
#Copyright (c) 1998 by ZeeGee Software Inc.
#All rights reserved.  This program is free software; you can redistribute
#it and/or modify it under the same terms as Perl itself.
#
#This software comes with B<NO WARRANTY> of any kind.
#See the COPYING file in the distribution for details.
#
#Eryq (F<eryq@zeegee.com>).
#President, ZeeGee Software Inc. (F<http://www.zeegee.com>).
#
#Created: 11 December 1996.  Ho ho ho.
#
#
#
#
#
#
#This module has been modified by Infopop Corporation.
#Modifications are copyright 2001.
#
