PowerShell mit STARTTLS

Für gewöhnlich nutze ich Programme wie Blat oder Commandlets wie "Send-MaiMessage" (Siehe PowerShell und Mail) um eine Mail an ein System zu senden. Manchmal tut es aber auch ein Telnet (Sehe SMTP-Telnet). Da scheitere ich dann aber doch schnell, wenn ich Verschlüsselt per STARTTLS kommunizieren möchten. Die Anmeldung mit LOGIN oder PLAIN ist durchaus noch manuell möglich, wenn Sie Username und Kennwort mal schnell BASE64-codieren. Um aber das Verhalten der Gegenseite bei SMTP mit einer verschlüsselten Verbindung nachzugehen, habe ich mir ein PowerShell-Skript geschrieben, was schnell auf örtliche Gegebenheiten angepasst werden kann.

Einsatz

Das Skript habe ich hier einfach als Text eingebunden und sie können es einfach über die Zwischenablage kopieren und als PS1-Datei speichern. Alle Werte sind über Parameter steuerbar. Nur das Kennwort wird immer interaktiv abgefragt. Ich hinterlege ungern Kennworte aber auf PS Passwort / Kennwort habe ich ja Beispiele zu sicheren Ablage von Kennworten aufgezählt.

Das Skript ist klassischer "Spaghetticode" und vielleicht komme ich später noch mal dazu, immer wiederkehrende Codeteile als Funktion umzustellen. Es ist kein Skript für den täglichen Einsatz und Automatisierung sondern für eine klar umrissene Diagnosefunktion im Fehlerfall. So kann ich damit nach dem Wechsel mittels STARTTLS die dann angebotenen Anmeldeverfahren sehen.

Hier gut zu erkennen, dass Office 365 da schon XOAUTH2 unterstützt.

In einem anderen Fall hat ein Client Mails nicht versenden können und alle Analysen auf dem Client oder Server waren nicht hilfreich. Der Client hat gar keine Logs geschrieben und OpenSSL hat ja ein Problem mit dem "R" während einer STARTTLS-Session und Thunderbird hat den Benutzernamen mit "@computername" verändert. Mit dem Skript habe ich dann aber die echte Fehlermeldung des SMTP-Servers gesehen, die aber nur über eine verschlüsselte Verbindung sichtbar wurde. Vorher wurde die Authentifizierung ja gar nicht angeboten.

Skript

Hier das Skript, mit dem ich meine Tests durchführe. Es kommt ohne weitere AddOns aus

#
# Simple version to send a SMTP-message with Authentication and StartTLS
#

param (
    $smtpserver = "outlook.office365.com",
    $smtpPort = 587,
    $Subject = "SMTP-Test",
    $Body = "Test Body",
    $authuser = "user1@msxfaq.de",
    $mailfrom = "user2@msxfaq.de",
    $rcptto = "user4@uclabor.de",
    $headerfrom = "user5@msxfaq.de",
    $headerto = "user6@uclabor.de"
)

Write-host "------ Start ---------"
# Get Password from User to Authenticate
$cred = Get-Credential -username $authuser –Message "SMTP Username und Kennwort"
Write-host "  Auth username = $($cred.username)"

# Establish TCP-Connection to MailServer
Write-host ">CONNECT to Server:Port $($smtpserver):$($smtpPort)"
try {
    $tcpclient=New-Object System.Net.Sockets.TcpClient
    $null=$tcpclient.Connect($smtpserver, $smtpPort)
    Write-host "<CONNECTED  wait for prompt"

    # Bind Streams to  connection
    $tcpstream = New-Object System.Net.Sockets.NetworkStream($tcpclient.Client)
    $tcpreader = New-Object System.IO.StreamReader $tcpstream
    $tcpwriter = New-Object System.IO.StreamWriter $tcpstream
    $tcpwriter.AutoFlush = $true

    [string]$answer=$tcpreader.ReadLine()
    if ($answer.startswith("2")) {
        Write-host "<$($answer)"
    }
    else {
        Write-warning "Did no get 2 got $($answer)"
        exit
    }
}
catch {
    Write-warning "Unable to Connect - exit"
    exit
}

# Send EHLO and read first 2xx messages
Write-host ">EHLO $($env:computername)"
$tcpwriter.WriteLine("EHLO $($Env:Computername)")
[string]$answer=$tcpreader.ReadLine()
Write-host "<$($answer)"
[bool]$STARTTLSfound=$False
while ($tcpreader.Peek() -ne -1) {
    [string]$answer=$tcpreader.ReadLine()
    Write-host "<$($answer)"
    if ($answer.contains("STARTTLS")) {
        $STARTTLSfound=$True
    }
}
if (!$STARTTLSfound) {
    Write-warning "STARTLS not found in answer"
    exit
}

Write-host ">STARTTLS"
$tcpwriter.WriteLine("STARTTLS")
[string]$answer=$tcpreader.ReadLine()
if ($answer.startswith("2")) {
    Write-host "<$($answer)"
}
else {
    Write-warning "Did no get 2 got $($answer)"
    exit
}
# Bei dieser Antwort sollte 220 rauskommen,

# SSL Stream instanzierne und Handshake starten
$sslStream = New-Object System.Net.Security.SslStream($tcpstream,$false)
$sslStream.AuthenticateAsClient($smtpserver)

# Streams fuer weitere Kommunikation
$sslreader = New-Object System.IO.StreamReader $sslStream
$sslwriter = New-Object System.IO.StreamWriter $sslStream
$sslwriter.AutoFlush = $true

# SMTP-Handshake wieder starten
Write-host ">EHLO $($env:computername)"
$sslwriter.WriteLine("EHLO $($Env:Computername)")
start-sleep -Seconds 1
while ($sslreader.Peek() -gt -1) {
    $sslreader.ReadLine()
}

Write-host ">AUTH LOGIN"
$sslwriter.WriteLine("AUTH LOGIN")
[string]$answer=$sslreader.ReadLine()
if ($answer.startswith("334")) {
    Write-host "<$($answer)"
}
else {
    Write-warning "Did no get 334  got $($answer)"
    exit
}

$userBytes = [System.Text.Encoding]::ASCII.GetBytes($cred.username)
$userBase64 = [System.Convert]::ToBase64String($userBytes)
Write-host ">SendUsername $($userbase64)"
$sslwriter.WriteLine($userBase64)
[string]$answer=$sslreader.ReadLine()
if ($answer.startswith("334")) {
    Write-host "<$($answer)"
}
else {
    Write-warning "Did no get 334  got $($answer)"
    exit
}

$pass = $cred.GetNetworkCredential().Password
$passBytes = [System.Text.Encoding]::ASCII.GetBytes($pass)
$passBase64 = [System.Convert]::ToBase64String($passBytes)
Write-host ">SendPassword $($passbase64)"
$sslwriter.WriteLine($passBase64)
[string]$answer=$sslreader.ReadLine()
if ($answer.startswith("2")) {
    Write-host "<$($answer)"
}
else {
    Write-warning "Did no get 2xx got $($answer)"
    exit
}

Write-host ">MAIL FROM:$($mailfrom)"
$sslwriter.WriteLine("MAIL FROM:$($mailfrom)")
[string]$answer=$sslreader.ReadLine()
if ($answer.startswith("2")) {
    Write-host "<$($answer)"
}
else {
    Write-warning "Did no get 2  got $($answer)"
    exit
}

Write-host ">RCPT TO:$($rcptto)"
$sslwriter.WriteLine("RCPT TO:$($rcptto)")
[string]$answer=$sslreader.ReadLine()
if ($answer.startswith("2")) {
    Write-host "<$($answer)"
}
else {
    Write-warning "Did no get 2.  got $($answer)"
    exit
}
Write-host ">DATA and wait x Sec"
$sslwriter.WriteLine("DATA")
[string]$answer=$sslreader.ReadLine()
if ($answer.startswith("3")) {
    Write-host "<$($answer)"
}
else {
    Write-warning "Did no get 3xx  got $($answer)"
    exit
}
Write-host ">Start Body"
Write-host(">From: $headerfrom")
$sslwriter.WriteLine("From: $headerfrom")
Write-host(">To: $headerto")
$sslwriter.WriteLine("To: $headerto")
Write-host(">Date: $((Get-Date).ToString(""ddd, d M y H:m:s z""))")
$sslwriter.WriteLine("Date: $((Get-Date).ToString(""ddd, d M y H:m:s z""))")
Write-host(">Subject: $Subject")
$sslwriter.WriteLine("Subject: $Subject")
Write-host(">")
$sslwriter.WriteLine("")
Write-host(">$Body")
$sslwriter.WriteLine("$Body")
Write-host(">.")
$sslwriter.WriteLine(".")
[string]$answer=$sslreader.ReadLine()
if ($answer.startswith("2")) {
    Write-host "<$($answer)"
}
else {
    Write-warning "Did no get 2  got $($answer)"
}

# Streams und Sockets schließen
Write-host "------Cleaning Sockets -----"
$sslwriter.Close();$sslreader.Close();$sslStream.Close()
$tcpreader.Close();$tcpwriter.Close();$tcpclient.Close()
Write-host "------ End ----------"

Ich habe das Skript absichtlich "einfach" gehalten. Sie sehen wie erst die TCP-Verbindung mit dem Stream aufgebaut wird und ich per StreamReader und StreamWriter dann die Texte sende und empange. Mit dem Wechsel auf HTTPS muss dann natürlich ein SSL-Stream gelesen und geschrieben werden.

Weitere Links