1

I'm not sure if this is an XY problem, but I have to aupport a horrible piece of software that won't talk to modern mail servers properly. It can only do SMTP and insists on providing AUTH ... even when the relay server does not offer or require it, sets the from address based on the username despite being separate fields in the protocol, and it will simply disconnect if it doesn't get 250-AUTH PLAIN ... in the server's response to EHLO. There are many unresolved complaints about this software online so I know the vendor will be of no help. I tried adding sasl auth to the SMTP server (Postfix) but did not have any success. I could not get it to verify credentials, and I fear that they will be passed on to the relayhost which will reject them. I couldn't find a way to get Postfix to ignore sent credentials either.

To try to get around all this, I wrote a basic script that listens on its own port, and proxies the SMTP communication, validating some arbitrary credentials during the process:

#!/bin/bash

user=<<REDACTED from>> pw=<<REDACTED password>>

function reset { send_auth=1 authing=0 ok=1 p=/tmp/smtp_backpipe if [[ -p $p ]]; then # drain pipe dd if=$p iflag=nonblock of=/dev/null 2>/dev/null else mkfifo $p fi }

function pw_check { if [[ "${1%%[[:space:]]}" == "${auth%%[[:space:]]}" ]]; then echo 235 2.7.0 Authentication successful > $p else echo 535 5.7.0 Authentication failed > $p ok=0 fi }

function changeo { while (( ok )) && IFS= read -r line; do case $1 in in) if (( authing )); then >&2 printf '%s\n' "$line" pw_check "$line" authing=0 elif [[ "$line" =~ ^AUTH[[:space:]]+PLAIN([[:space:]]+([^[:space:]]+))?[[:space:]]*$ ]]; then if [[ "${BASH_REMATCH[1]}" ]]; then >&2 printf '%s\n' "$line" pw_check "${BASH_REMATCH[2]}" else authing=1 echo 334 | tee -a /dev/stderr > $p fi else printf '%s\n' "$line" fi ;; out) if [[ ! "$line" =~ ^250[^[:alpha:]]+AUTH[[:space:]] ]]; then printf '%s\n' "$line" if (( send_auth )) && [[ "$line" =~ ^250[^[:alpha:]] ]]; then send_auth=0 echo 250-AUTH PLAIN fi fi ;; esac if ! (( ok )); then exit 1 fi done }

auth="printf '\0%s\0%s' &quot;$user&quot; &quot;$pw&quot; | base64" i=0 while true; do >&2 echo "$(( ++i ))" reset cat $p | tee -a /dev/stderr | netcat -4Clp $1 | changeo in | tee -a /dev/stderr | nc -4C 127.0.0.1 25 | changeo out > $p done

I run ./smtp_proxy 9025 and it works except the second nc immediately connects to the real SMTP server and begins a transaction which will time out:

$ ./smtp_proxy 9025
1
220 <<REDACTED host>> ESMTP Postfix
EHLO [10.0.0.99]
250-<<REDACTED host>>
250-AUTH PLAIN
250-PIPELINING
250-SIZE 10240000
250-VRFY
250-ETRN
250-ENHANCEDSTATUSCODES
250-8BITMIME
250-DSN
250-SMTPUTF8
250 CHUNKING
AUTH PLAIN <<REDACTED auth>>
235 2.7.0 Authentication successful
MAIL FROM:<<<REDACTED from>>>
250 2.1.0 Ok
RCPT TO:<<<REDACTED to>>>
250 2.1.5 Ok
DATA
354 End data with <CR><LF>.<CR><LF>
From: <<REDACTED from>>
Subject: hi
To: <<REDACTED to>>
Content-Type: text/plain; charset=windows-1252; format=flowed
Content-Transfer-Encoding: 7bit

there . 250 2.0.0 Ok: queued as F23DFE0A1B QUIT 221 2.0.0 Bye 2 220 <<REDACTED host>> ESMTP Postfix 421 4.4.2 <<REDACTED host>> Error: timeout exceeded

I was hoping I could somehow delay the second nc invocation until it receives data from the pipeline, maybe by wrapping it in a function or something. socat seemed to do this on an unmodified connection:

socat -v tcp4-l:9025,crlf tcp4:127.0.0.1:25,crlf

...but as soon as I try to modify the streams by putting it in a pipeline, it does the same thing as nc, immediate connection to the SMTP server. (Replace the ncs above with socat TCP4-LISTEN:$1,crlf - and socat - TCP4:127.0.0.1:25,crlf respectively.)

Walf
  • 1,321
  • 1
    Your best option might be stick one of the options form https://unix.stackexchange.com/q/33049/70524 before nc (e.g., ... | { until read -t 0; do sleep 1; done; nc ...; } | ..., though read -t 0 is unreliable, so use one of the other options) – muru Mar 05 '21 at 08:39
  • Thanks, @muru. With your pointer, I was able to do exactly what I was trying to, even though it wasn't ultimately successful. – Walf Mar 06 '21 at 08:24
  • Got sasl working. – Walf Mar 09 '21 at 03:21

1 Answers1

0

Based on some of the answers @muru linked to, plus a more general one about overcoming read returning false when input doesn't end with a newline, I came up with a function that actually does delay starting a program until it receives input:

function until_input {
    >&2 echo waiting for input # debug only, remove this line
    local line
    local nl=0
    local format='%s'
    while IFS= read -r line; do
        nl=1
        format='%s\n'
        break
    done
    >&2 echo done waiting # debug only, remove this line
    if (( nl )) || [[ "$line" ]]; then
        >&2 echo was input # debug only, remove this line
        { printf "$format" "$line"; cat; } | "$@"
    fi
}

To prove that it works, see the following test which shows both the initial statement group and the until_input start in parallel as expected, but delayed task (here cat) does not start until input does.

{ echo wait >&2; sleep 2; echo go >&2; echo -n pipe$'\n'it baby; } | until_input cat

Allthough this seems to be an okay solution for the general problem of awaiting input, unfortunately, it did not solve my problem. I put until_input nc -4C 127.0.0.1 25 in the pipeline, then discovered what I'd need is for the second netcat to start as soon as someone connects to the first, before any line is sent, because the server responds first in SMTP. I guess I'll just have to debug the sasl with the help of socat.

Walf
  • 1,321