############################################################################### # Smtp.pm # # $Date: 01.09.2026 $ # ############################################################################### # YaBB: Yet another Bulletin Board # # Open-Source Community Software for Webmasters # # Version: YaBBForum 3.1 # # Packaged: 01 Sep, 2026 # # Distributed by: https://yabbforum.nz # # =========================================================================== # # Copyright (c) 2000-2026 YaBB (yabbforum.nz) - All Rights Reserved. # # Software by: The YaBB Development Team # # with assistance from the YaBB community. # ############################################################################### use English '-no_match_vars'; our $VERSION = '3.1'; $smtppmver = 'YaBBForum 3.1'; if ( $action eq 'detailedversion' ) { return 1; } eval q{ use IO::Socket::INET; use Digest::HMAC_MD5 qw(hmac_md5_hex); }; LoadLanguage('Smtp'); sub use_smtp { my ($smtpaddr); $OUTPUT_AUTOFLUSH = 1; my ($proto) = ( getprotobyname 'tcp' )[2]; my ($port) = ( getservbyname 'smtp', 'tcp' )[2] || 25; if ( $smtp_server =~ /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/xsm ) { $smtpaddr = ( $smtp_server =~ /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/xsm ) ? pack( 'C4', $1, $2, $3, $4 ) : ( gethostbyname $smtp_server )[4]; } $sendlog = q{}; $auth_ok = 0; # Connect to the SMTP server. $sock = IO::Socket::INET->new( PeerAddr => $smtp_server, PeerPort => $port, Proto => 'tcp', Timeout => 5 ) # Check if the service is available and parse any errors or fatal_error('smtp_unavail'); get_line(); say_hello($smtp_server) or exit 1; if ( $features{'AUTH'} && $smtp_auth_required ) { # Try CRAM-MD5 if supported by the server if ( $auth_ok == 0 && ( $features{'AUTH'} =~ /CRAM-MD5/ixsm || $smtp_auth_required == 3 || $smtp_auth_required == 4 ) ) { send_line("AUTH CRAM-MD5\r\n"); ( $code, $text, $more ) = get_line(); if ( $code != 334 && $smtp_auth_required != 4 ) { fatal_error( 'smtp_error', "[$code]: $smtp_txt{$code}

$smtp_txt{'5'}
$sendlog" ); } my $response = encode_cram_md5( $text, $authuser, $authpass ); send_line( "%s\r\n", $response ); ( $code, $text, $more ) = get_line(); if ( $code != 235 && $smtp_auth_required != 4 ) { fatal_error( 'smtp_error', "[$code]: $smtp_txt{$code}

$smtp_txt{'5'}
$sendlog" ); } $auth_ok = 1; } # Or try LOGIN method elsif ( $auth_ok == 0 && ( $features{'AUTH'} =~ /LOGIN/ism || $smtp_auth_required == 2 || $smtp_auth_required == 4 ) ) { send_line("AUTH LOGIN\r\n"); ( $code, $text, $more ) = get_line(); if ( $code != 334 && $smtp_auth_required != 4 ) { fatal_error( 'smtp_error', "[$code]: $smtp_txt{$code}

$smtp_txt{'5'}
$sendlog" ); } send_line( "%s\r\n", encode_smtp64( $authuser, q{} ) ); ( $code, $text, $more ) = get_line(); if ( $code != 334 && $smtp_auth_required != 4 ) { fatal_error( 'smtp_error', "[$code]: $smtp_txt{$code}

$smtp_txt{'5'}
$sendlog" ); } send_line( "%s\r\n", encode_smtp64( $authpass, q{} ) ); ( $code, $text, $more ) = get_line(); if ( $code != 235 && $smtp_auth_required != 4 ) { fatal_error( 'smtp_error', "[$code]: $smtp_txt{$code}

$smtp_txt{'5'}
$sendlog" ); } $auth_ok = 1; } # Or finally PLAIN if nothing else was supported. elsif ( $auth_ok == 0 && ( $features{'AUTH'} =~ /PLAIN/ism || $smtp_auth_required == 1 || $smtp_auth_required == 4 ) ) { send_line( "AUTH PLAIN %s\r\n", encode_smtp64( "$authuser\0$authuser\0$authpass", q{} ) ); ( $code, $text, $more ) = get_line(); if ( $code != 235 && $smtp_auth_required != 4 ) { fatal_error( 'smtp_error', "[$code]: $smtp_txt{$code}

$smtp_txt{'5'}
$sendlog" ); } $auth_ok = 1; } # Decide to complain about advertised methods not supported. else { fatal_error( 'smtp_error', "$smtp_txt{'notsupported'}

$smtp_txt{'5'}
$sendlog" ); } } # build the Date per RFC822 - uses gmtime to create date & time stamp ( $smtpsec, $smtpmin, $smtphour, $smtpmday, $smtpmon, $smtpyear, $smtpwday, $smtpyday, $smtpisdst ) = gmtime( $date ); $smtpyear = sprintf '%02d', ( $smtpyear - 100 ); $smtphour = sprintf '%02d', $smtphour; $smtpmin = sprintf '%02d', $smtpmin; $smtpsec = sprintf '%02d', $smtpsec; my @months2 = qw( Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec ); $smtpyear = qq~20$smtpyear~; $smtptimestring = qq~$days_short[$smtpwday], $smtpmday $months2[$smtpmon] $smtpyear $smtphour\:$smtpmin\:$smtpsec +0000~; # Fill the mail from field send_line("MAIL FROM: <$smtp_from>\r\n"); ( $code, $text, $more ) = get_line(); # Add as many addressees as needed foreach ( split /,\ /xsm, $smtp_to ) { send_line("RCPT TO: <$_>\r\n"); ( $code, $text, $more ) = get_line(); } # Send message data send_line("DATA\r\n"); ( $code, $text, $more ) = get_line(); send_line("To: $toheader\r\n"); send_line("Date: $smtptimestring\r\n"); send_line("From: $fromheader\r\n"); send_line("X-Mailer: YaBB SMTP\r\n"); send_line("Subject: $smtp_subject\r\n"); send_line("Content-Type: text/html\; charset=$smtp_charset\r\n\r\n"); send_line("$smtp_message"); send_line("\r\n.\r\n"); # It is polite to close the door behind you send_line("QUIT\r\n"); if ( $smtp_from eq q{} ) { $proto_error = "$smtp_txt{'no_from'}
"; } if ( $smtp_to eq q{} ) { $proto_error .= "$smtp_txt{'no_to'}
"; } if ($proto_error) { fatal_error( 'smtp_error', "
$proto_error
$sendlog" ); } return 1; } # Get one line of response from the server. sub get_line { my ( $code, $sep, $text ) = ( $sock->getline() =~ /(\d+)(.)([^\r]*)/xsm ); my $more; $code =~ s/ //gsm; if ( $sep eq q{-} ) { $more = 1; } else { $more = 0; } $sendlog .= qq~S:$code $text $sep~; $sendlog .= q~
~; return ( $code, $text, $more ); } # Send one line back to the server sub send_line (@) { my @args = @_; # $args[0] =~ s/\n/\r\n/gsm; $sendlog .= qq~C:$args[0]~; $sendlog =~ s/\r\n//gxsm; $sendlog .= q~
~; $sock->printf(@args); return; } # Helper function to encode CRAM-MD5 challenge sub encode_cram_md5 ($$$) { my ( $ticket64, $username, $password ) = @_; my $ticket = decode_smtp64($ticket64) or die "Unable to decode Base64 encoded string '$ticket64'\n"; my $password_md5 = hmac_md5_hex( $ticket, $password ); return encode_smtp64( "$username $password_md5", q{} ); } sub encode_smtp64 { my ( $inp, $eol ) = @_; if ( $] >= 5.006 ) { require bytes; if ( bytes::length($inp) > length($inp) || ( $] >= 5.008 && $inp =~ /[^\0-\xFF]/xsm ) ) { require Carp; Carp::croak('The Base64 encoding is only defined for bytes'); } } require integer; import integer; if ( !$eol ) { $eol = "\n"; } my $res = pack 'u', $inp; # Remove first character of each line, remove newlines $res =~ s/^.//gxsm; $res =~ s/\n//gxsm; $res =~ tr|` -_|AA-Za-z0-9+/|; # `# help emacs # fix padding at the end my $padding = ( 3 - length($inp) % 3 ) % 3; if ($padding) { $res =~ s/.{$padding}$/q{=} x $padding/exsm; } # break encoded string into lines of no more than 76 characters each if ( length $eol ) { $res =~ s/(.{1,76})/$1$eol/gxsm; } chomp $res; return $res; } sub decode_smtp64 ($) { local $WARNING = 0; # unpack("u",...) gives bogus warning in 5.00[123] require integer; import integer; my $str = shift; $str =~ tr|A-Za-z0-9+=/||cd; # remove non-base64 chars $str =~ s/=+$//xsm; # remove padding $str =~ tr|A-Za-z0-9+/| -_|; # convert to uuencoded format if ( !length $str ) { return q{}; } my $uustr = q{}; my $l = length($str) - 60; foreach my $i ( 0 .. $l ) { if ( $i % 60 == 0 ) { $uustr .= 'M' . substr $str, $i, 60; } } $str = substr $str, $i; # and any leftover chars if ( $str ne q{} ) { $uustr .= chr( 32 + length($str) * 3 / 4 ) . $str; } return unpack 'u', $uustr; } sub say_hello ($) { my ($hello_host) = @_; my ( $feat, $param ); #send RFC2821 compliant identifier send_line("EHLO $hello_host\r\n"); ( $code, $text, $more ) = get_line(); if ( $code != 250 ) { #try sending an old RFC281 compliant identifier (older Exchange servers) send_line("HELO $hello_host\r\n"); } ( $code, $text, $more ) = get_line(); if ( $code == 250 ) { read_features( \%features ); } return 1; } sub read_features ($) { my ($featref) = @_; # Empty the hash %{$featref} = (); ( $feat, $param ) = ( $text =~ /^(\w+)[= ]*(.*)$/xsm ); $featref->{$feat} = $param; # Load all features presented by the server into the hash while ( $more == 1 ) { ( $code, $text, $more ) = get_line(); ( $feat, $param ) = ( $text =~ /^(\w+)[= ]*(.*)$/xsm ); $featref->{$feat} = $param; } return 1; } 1;