#!/usr/bin/env perl

# Revision History:
# 2026-10-27 Claude v1.0.0 bug 213: send_local_smtp (sendmail shim) and send_auth_smtp (fake SMTP server, missing-module die)

use strict;
use warnings;
use Test::More;
use File::Temp qw(tempdir);
use IO::Socket::INET;

my $tmp = tempdir( CLEANUP => 1 );

sub slurp { open my $f, '<', $_[0] or return ''; local $/; my $d = <$f>; close $f; return $d; }

# load a transport the way the client does: do-file into main, then call main::doit
sub load {
   my $script = shift;
   { no strict 'refs'; undef &main::doit if defined &main::doit; }
   my $r = do $script;
   die "load $script: " . ( $@ || $! ) unless $r;
}

# ---------- send_local_smtp ----------
my $shim = "$tmp/sendmail";
open my $s, '>', $shim or die;
print {$s} qq{#!/bin/sh\necho "\$*" > $tmp/args\ncat > $tmp/msg\nexit \${SHIM_EXIT:-0}\n};
close $s;
chmod 0755, $shim;

load('./unix/transports/send_local_smtp');
my %p = ( sendmailPath => $shim, mailTo => 'a@example.com, b@example.com', mailFrom => 'me@example.com',
          mailCC => 'c@example.com', mailBCC => 'secret@example.com', mailSubject => "Hi\r\nBcc: evil\@example.com" );

is( main::doit( {%p}, "line1\nline2\n" ), 1, 'local: success returns 1' );
my $msg = slurp("$tmp/msg");
like( slurp("$tmp/args"), qr/^-i -f me\@example\.com -- a\@example\.com b\@example\.com c\@example\.com secret\@example\.com$/, 'local: envelope has from and all recipients' );
like( $msg, qr/^To: a\@example\.com, b\@example\.com$/m, 'local: To header' );
like( $msg, qr/^Cc: c\@example\.com$/m, 'local: Cc header' );
unlike( $msg, qr/^Bcc:/mi, 'local: no Bcc header, and subject cannot inject one' );
like( $msg, qr/^Subject: Hi Bcc: evil\@example\.com$/m, 'local: CR/LF in subject neutralised' );
like( $msg, qr/\n\nline1\nline2\n$/, 'local: body follows blank line' );

{
   local $ENV{SHIM_EXIT} = 1;
   my $rc = main::doit( {%p}, 'x' );
   isnt( $rc, 1, 'local: sendmail exit 1 is not reported as success' );
   is( $rc, 101, 'local: exit status mapped to 100 + n' );
}
{
   local $ENV{SHIM_EXIT} = 75;
   is( main::doit( {%p}, 'x' ), 175, 'local: exit 75 -> 175' );
}
{
   local $ENV{SHIM_EXIT} = 0;
   is( main::doit( { %p, mailTo => '' }, 'x' ), 2, 'local: missing mailTo -> 2' );
   is( main::doit( { %p, sendmailPath => "$tmp/nonexistent" }, 'x' ), 3, 'local: missing sendmail -> 3' );
   is( main::doit( { %p, attachment => "$tmp/nope" }, 'x' ), 4, 'local: unreadable attachment -> 4' );
}
open my $a, '>', "$tmp/att.txt" or die; print {$a} "attached\n"; close $a;
is( main::doit( { %p, attachment => "$tmp/att.txt" }, 'body' ), 1, 'local: attachment sends' );
$msg = slurp("$tmp/msg");
like( $msg, qr/multipart\/mixed/, 'local: multipart with attachment' );
like( $msg, qr/filename="att\.txt"/, 'local: attachment filename' );
like( $msg, qr/\nYXR0YWNoZWQK\n/, 'local: attachment base64' );

# ---------- send_auth_smtp ----------
load('./unix/transports/send_auth_smtp');
is( main::doit( { mailTo => 'a@example.com' }, 'x' ), 2, 'auth: missing mailServer -> 2' );
is( main::doit( { mailServer => '127.0.0.1:1', mailTo => 'a@example.com', tls => 'no' }, 'x' ), 3, 'auth: connection refused -> 3' );

# a tiny fake SMTP server (no TLS, no AUTH), one connection, message saved to a file
my $listen = IO::Socket::INET->new( Listen => 1, LocalAddr => '127.0.0.1', LocalPort => 0, Proto => 'tcp' ) or die $!;
my $port = $listen->sockport;
my $pid = fork;
die unless defined $pid;
if ( !$pid ) {
   my $c = $listen->accept; $c->autoflush(1);
   print $c "220 fake ESMTP\r\n";
   my $data = 0; my $got = '';
   while ( my $l = <$c> ) {
      if ( $data ) { if ( $l eq ".\r\n" ) { $data = 0; print $c "250 queued\r\n"; } else { $got .= $l; } next; }
      if    ( $l =~ /^EHLO/i ) { print $c "250-fake\r\n250 8BITMIME\r\n"; }
      elsif ( $l =~ /^(MAIL|RCPT)/i ) { open my $o, '>>', "$tmp/smtp_env" or die; print {$o} $l; close $o; print $c "250 ok\r\n"; }
      elsif ( $l =~ /^DATA/i ) { $data = 1; print $c "354 go\r\n"; }
      elsif ( $l =~ /^QUIT/i ) { print $c "221 bye\r\n"; last; }
      else { print $c "250 ok\r\n"; }
   }
   open my $o, '>', "$tmp/smtp_msg" or die; print {$o} $got; close $o;
   exit 0;
}
close $listen;
is( main::doit( { mailServer => "127.0.0.1:$port", tls => 'no', mailTo => 'a@example.com', mailBCC => 'b@example.com',
                  mailFrom => 'me@example.com', mailSubject => 'S' }, "report\n" ), 1, 'auth: sends to a plain server with tls: no' );
waitpid $pid, 0;
like( slurp("$tmp/smtp_env"), qr/MAIL FROM:<me\@example\.com>.*RCPT TO:<a\@example\.com>.*RCPT TO:<b\@example\.com>/s, 'auth: envelope sender and both recipients' );
like( slurp("$tmp/smtp_msg"), qr/^Subject: S\r?\n.*report/ms, 'auth: message delivered' );

# a missing module must die, naming the module
{
   my $blocked = 'IO/Socket/SSL.pm';
   local @INC = ( sub { return unless $_[1] eq $blocked || $_[1] eq 'Authen/SASL.pm'; die "Can't locate $_[1] in \@INC\n" }, @INC );
   local %INC = %INC; delete $INC{$blocked}; delete $INC{'Authen/SASL.pm'};
   my $rc = eval { main::doit( { mailServer => '127.0.0.1:25', mailTo => 'a@example.com' }, 'x' ) };
   like( $@, qr/required Perl module IO::Socket::SSL is not installed/, 'auth: missing IO::Socket::SSL dies naming it' );
   $rc = eval { main::doit( { mailServer => '127.0.0.1:25', mailTo => 'a@example.com', tls => 'no', smtpUser => 'u', smtpPass => 'p' }, 'x' ) };
   like( $@, qr/required Perl module Authen::SASL is not installed/, 'auth: missing Authen::SASL dies naming it' );
   $@ = '';
   $rc = eval { main::doit( { mailServer => '127.0.0.1:1', mailTo => 'a@example.com', tls => 'no' }, 'x' ) };
   is( $@, '', 'auth: tls no, no user needs neither optional module' );
}

done_testing();
