Search code examples
delphisslsmtpindy

Which TIdSMTP/SSLIOHandler event should trigger when TLS negotiation fails?


I want to make SMTP TLS "implicit/explicit" configuration transparent, and to do this I thought of using event triggers.

My idea is just to capture if the TLS negotiation succeeds or fails and then try again with the other option.

I saw some events on TIdSMTP and TIdSSLIOHandlerSocketOpenSSL, but none of them seems to trigger correctly.

Documentation for Indy also can't be downloaded here (DNS broken error).

I used the following events with a "Sleep(1);" just to put a breakpoint in them.

TIdSMTP:

OnTLSNotAvailable

OnTLSHandShakeFailed

OnTLSNegCmdFailed

OnFailedEHLO

TIdSSLIOHandlerSocketOpenSSL:

OnStatus

OnStatusInfo

OnStatusInfoEx

What I've tested:

I used for example Outlook smtp.office365.com with port 587 (which is STARTTLS - utUseExplicitTLS in Indy), but I intentionally changed the TIdSMTP.UseTLS to utUseImlicitTLS to simulate a TLS negotiation failure.

In the first TIdSMTP.Connect() test, I got an EIdSocketError exception ('Socket Error # 10060 Connection timed out') instead of a TLS negotiation failed message/exception. In this test, the only triggered events were TIdSSLIOHandlerSocketOpenSSL.OnStatus and TIdSMTP.OnStatus.

In the second TIdSMTP.Connect() test, I don't receive the EIdSocketError with timeout message, and aside from the events on the first try, it also triggered the TIdSSLIOHandlerSocketOpenSSL.OnStatusInfo and TIdSSLIOHandlerSocketOpenSSL.OnStatusInfoEx events, handling TLS connection error.

Some code snippet:

procedure Connect;
var
  LSMTP: TIdSMTP;
  LSSL: TIdSSLIOHandlerSocketOpenSSL;
begin
  LSMTP:= TIdSMTP.Create(nil);
  LSSL := TIdSSLIOHandlerSocketOpenSSL.Create(nil);
  
  {functions to be declared}
  LSMTP.OnTLSNotAvailable   := MyTLSNotAvailable;
  LSMTP.OnTLSHandShakeFailed:= MyTLSHandShakeFailed;
  LSMTP.OnTLSNegCmdFailed   := MyTLSNegCmdFailed;
  LSMTP.OnFailedEHLO        := MyFailedEHLO;

  LSSL.OnStatus      := MyOnStatus;
  LSSL.OnStatusInfo  := MySSLInfo;
  LSSL.OnStatusInfoEx:= MySSlInfoEx;
  LSSL.ConnectTimeout:= 1000000;
  LSSL.ReadTimeout   := 1000000;

  LSMTP.IOHandler       := FSSL;
  LSMTP.AuthType        := satDefault;
  LSMTP.UseTLS          := utUseExplicitTLS;
  LSSL.SSLOptions.Method:= sslvSSLv23;
  LSSL.SSLOptions.Mode  := sslmBoth;

  LSMTP.Host:= 'smtp.office365.com';
  LSMTP.Port:= '587';
  LSMTP.Username:= [email protected];
  LSMTP.Password:= password;
  LSMTP.From:= LSMTP.Username;

  try
    LSMTP.Connect;
  except
      raise Exception.Create(Format('Error during connect attempt: %s',[Exception(ExceptObject).Message]));
  end;

end;

So, I have some questions:

  • Why don't the TIdSMTP.OnTLSNotAvailable, TIdSMTP.OnTLSHandShakeFailed, and TIdSMTP.OnTLSNegCmdFailed events trigger in those cases?

  • Why do the TIdSSLIOHandlerSocketOpenSSL.OnStatusInfo and TIdSSLIOHandlerSocketOpenSSL.OnStatusInfoEx events trigger only in the second connection try?

Am I missing something?


Solution

  • You should not use events to accomplish what you are attempting, as Indy is not event-driven. Events are for informational purposes only, not for flow control. If the TLS handshake fails, an exception will be raised accordingly. Catch the exception, close the TCP connection, and retry as needed.

    As for your attempt to simulate a TLS failure:

    • If you use utUseImplicitTLS on a non-tls or explicit-tls SMTP port, the client will send a TLS handshake immediately upon establishing the TCP connection, and then fail the TLS handshake when it misinterprets the server's unencrypted SMTP greeting as-if it were a TLS handshake response.

    • If you use utUseExplicitTLS on an implicit-tls port, the client will timeout (or deadlock if you don't use a timeout) after establishing the TCP connection, because it will be waiting for an unencrypted SMTP greeting that the server never sends, because the server is waiting for a TLS handshake that the client never sends.

    Regarding the various events that are available:

    • OnTLSNotAvailable is fired when:

      • UseTLS is utUseRequireTLS 1, and

      • UseEhlo is True, and

      • the SMTP server does not advertise support for STARTTLS when the client sends an EHLO command to learn the server's capabilities.

      If the event handler sets the VContinue parameter to False then an EIdTLSClientTLSNotAvailable exception is raised, otherwise the SMTP session continues unencrypted. The default is True.

      1: On the client side, utUseRequireTLS is similar to utUseExplicitTLS, but a server's refusal to use TLS is not acceptable. Whereas TLS is optional with utUseExplicitTLS.

    • OnTLSHandShakeFailed is fired when:

      • UseTLS is utUseImplicitTLS or utUseExplicitTLS (not utUseRequireTLS), and

      • the SSLIOHandler raises an exception during the TLS handshake.

      If the event handler sets the VContinue parameter to False then an EIdTLSClientTLSHandShakeFailed exception is raised, otherwise the SMTP session continues unencrypted. The default is False.

      NOTE: in this case, if the exception is bypassed then the SMTP session will technically be in undefined behavior territory, as the state of the socket's data will be unknown (was the exception raised in the middle of a packet? In between packets? Are more packets still in-flight? Etc). Unlike with the other events where the socket data is left in a well-defined state.

    • OnTLSNegCmdFailed is fired when:

      • UseTLS is utUseExplicitTLS (not utUseRequireTLS), and

      • the server sends a failure response to the STARTTLS command before a TLS handshake begins.

      If the event handler sets the VContinue parameter to False then an EIdTLSClientTLSNegCmdFailed exception is raised, otherwise the SMTP session continues unencrypted. The default is False.

    • OnFailedEHLO is fired when:

      • UseEhlo is True, and

      • the server sends a failure response to the EHLO command.

      If the event handler sets the VContinue parameter to False then the TCP connection is closed and an EIdSMTPReplyError exception is raised, otherwise the SMTP session continues without knowing what the server's capabilities are. The default is True.

    • OnStatus is fired for some general-purpose component-level actions, like resolving the IP address of a hostname, connecting to a TCP port, encoding email, etc.

    • OnStatusInfo/Ex are fired by the SSLIOHandler to provide status updates from the OpenSSL library during a TLS session. Refer to OpenSSL's documentation for information about what kind of status values are provided:

      https://www.openssl.org/docs/man1.0.2/man3/SSL_CTX_set_info_callback.html