Okay all, here's a workaround for this problem until ST can get this resolved. I'll add lower level detail at the end of this reply.
Run the following PowerShell script interactively as an administrator.
#Begin
$TypeDefinition = @"
using System;
using System.Collections;
using System.Collections.Generic;
using System.Net.Sockets;
using System.Runtime.InteropServices;
using System.Text;
namespace ConnectionKiller
{
public class Program
{
// Taken from
https://github.com/yromen/repository/tree/master/DNProcessKiller // It part from the Disconnecter class.
// In case of nested class use "+" like that [ConnectionKiller.Program+Disconnecter]::Connections()
/// <summary>
/// Enumeration of the states
/// </summary>
public enum State
{
/// <summary> All </summary>
All = 0,
/// <summary> Closed </summary>
Closed = 1,
/// <summary> Listen </summary>
Listen = 2,
/// <summary> Syn_Sent </summary>
Syn_Sent = 3,
/// <summary> Syn_Rcvd </summary>
Syn_Rcvd = 4,
/// <summary> Established </summary>
Established = 5,
/// <summary> Fin_Wait1 </summary>
Fin_Wait1 = 6,
/// <summary> Fin_Wait2 </summary>
Fin_Wait2 = 7,
/// <summary> Close_Wait </summary>
Close_Wait = 8,
/// <summary> Closing </summary>
Closing = 9,
/// <summary> Last_Ack </summary>
Last_Ack = 10,
/// <summary> Time_Wait </summary>
Time_Wait = 11,
/// <summary> Delete_TCB </summary>
Delete_TCB = 12
}
/// <summary>
/// Connection info
/// </summary>
private struct MIB_TCPROW
{
public int dwState;
public int dwLocalAddr;
public int dwLocalPort;
public int dwRemoteAddr;
public int dwRemotePort;
}
//API to change status of connection
[DllImport("iphlpapi.dll")]
//private static extern int SetTcpEntry(MIB_TCPROW tcprow);
private static extern int SetTcpEntry(IntPtr pTcprow);
//Convert 16-bit value from network to host byte order
[DllImport("wsock32.dll")]
private static extern int ntohs(int netshort);
//Convert 16-bit value back again
[DllImport("wsock32.dll")]
private static extern int htons(int netshort);
/// <summary>
/// Close a connection by returning the connectionstring
/// </summary>
/// <param name="connectionstring"></param>
public static void CloseConnection(string localAddress, int localPort, string remoteAddress, int remotePort)
{
try
{
//if (parts.Length != 4) throw new Exception("Invalid connectionstring - use the one provided by Connections.");
string[] locaddr = localAddress.Split('.');
string[] remaddr = remoteAddress.Split('.');
//Fill structure with data
MIB_TCPROW row = new MIB_TCPROW();
row.dwState = 12;
byte[] bLocAddr = new byte[] { byte.Parse(locaddr[0]), byte.Parse(locaddr[1]), byte.Parse(locaddr[2]), byte.Parse(locaddr[3]) };
byte[] bRemAddr = new byte[] { byte.Parse(remaddr[0]), byte.Parse(remaddr[1]), byte.Parse(remaddr[2]), byte.Parse(remaddr[3]) };
row.dwLocalAddr = BitConverter.ToInt32(bLocAddr, 0);
row.dwRemoteAddr = BitConverter.ToInt32(bRemAddr, 0);
row.dwLocalPort = htons(localPort);
row.dwRemotePort = htons(remotePort);
//Make copy of the structure into memory and use the pointer to call SetTcpEntry
IntPtr ptr = GetPtrToNewObject(row);
int ret = SetTcpEntry(ptr);
if (ret == -1) throw new Exception("Unsuccessful");
if (ret == 65) throw new Exception("User has no sufficient privilege to execute this API successfully");
if (ret == 87) throw new Exception("Specified port is not in state to be closed down");
if (ret == 317) throw new Exception("The function is unable to set the TCP entry since the application is running non-elevated");
if (ret != 0) throw new Exception("Unknown error (" + ret + ")");
}
catch (Exception ex)
{
throw new Exception("CloseConnection failed (" + localAddress + ":" + localPort + "->" + remoteAddress + ":" + remotePort + ")! [" + ex.GetType().ToString() + "," + ex.Message + "]");
}
}
private static IntPtr GetPtrToNewObject(object obj)
{
IntPtr ptr = Marshal.AllocCoTaskMem(Marshal.SizeOf(obj));
Marshal.StructureToPtr(obj, ptr, false);
return ptr;
}
}
}
"@
Add-Type -TypeDefinition $TypeDefinition -PassThru | Out-Null
while ($true)
{
$SmtpConnections = Get-NetTCPConnection -RemotePort 25 -State Established -ErrorAction SilentlyContinue| Where-Object -Property "CreationTime" -LT $((Get-Date).AddMinutes(-30))
if ($SmtpConnections)
{
foreach ($Connection in $SmtpConnections)
{
Write-Host "Issue found at $(Get-Date) with connection`nRemote address`t" $Connection.RemoteAddress "`nLocal port`t`t" $Connection.LocalPort
[ConnectionKiller.Program]::CloseConnection($connection.LocalAddress, $connection.LocalPort, $connection.RemoteAddress, $connection.RemotePort)
}
}
else
{
Write-Host "No issues found at $(Get-Date)"
}
Start-Sleep -Seconds 900
}
#End
Low level;
The System.Net.Security.SslStream remains open when there are errors in the stream, the connection can be seen at the network level and remains in the 'established' state. The spooler holds the email in the processing state and so the mail is never retried. A restart of the service clears all established connections, which is why that has been the workaround this far.
The application needs to retry the SMTP command after 60 seconds if a response is not received (or a 'TCP Dup ACK' is seen), the application should also retry using STARTTLS if the message returns to spool processing instead of reverting to clear text. If the STARTTLS option is there it should be used.
My script looks for stale SMTP connections that have been open for more than 30 minutes and ends them.