-
Notifications
You must be signed in to change notification settings - Fork 1.1k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
10210 Improve connection error handling in ESMTPSenderFactory #1645
base: trunk
Are you sure you want to change the base?
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't have time for a full review now but hopefully these initial thoughts are useful!
src/twisted/mail/test/test_smtp.py
Outdated
raise Exception("ESMTPSenderFactory unexpectedly completed") | ||
|
||
sentDeferred.addCallbacks(callback, errback) | ||
return sentDeferred |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In general we now strongly prefer the style of test where you don't spin up a real reactor and do for-real network I/O; instead, consider using the various utilities in twisted.internet.testing
to produce deterministic mocks for your network set-up attempts. That said, this style of test is definitely better than nothing, so if you feel that the deterministic version would take you a bunch more time we can work with this for now (it's certainly not the first such test in this file).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the speedy initial review, @glyph - and sorry for the slow turnaround on my side.
Yeah, I'm aware of the preference. I initially just went with the flow in this file, but I've just taken a bit of a harder look at what it would take to use twisted.test.iosim.FakeTransport
and friends to connect up the client and server.
It would be a bit fiddly, but the real problem is that what I really want to test here is how _newtls.startTLS
interacts with the SMTP code, and startTLS
relies on wrapping a real FileDescriptor
object - or at least, something that behaves much the same as one. Possibly I could rejig FakeTransport
enough to make startTLS
work with it ... but at that point I start to feel like I'm just testing the tests.
So, as painful as it is, I think this might be a case where we're better off with a real reactor, at least for now.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No need to be sorry for a slow turnaround. We're all very time-constrained here in open source land, and we do things when we can.
ca3d15d
to
6619a0e
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks so much for improving this error handling. I hope that this feedback is minor and you an address so we can land soon?
src/twisted/mail/smtp.py
Outdated
# If the initial connection succeeded, but there was a later error | ||
# (such as TLS validation failure), we're more interested in that than | ||
# "Connection was closed cleanly" or whatever. | ||
if self.currentProtocol and self.currentProtocol.failureReason: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
- We don't want to test
self.currentProtocol.__bool__
here, we just want to check if itis not None
- I find that inline ternaries are more expressive than overloading boolean logic for tests like this, so, for clarity, I think I'd prefer this:
if self.currentProtocol and self.currentProtocol.failureReason: | |
if self.currentProtocol.failureReason is not None if self.currentProtocol is not None else False: |
The first point is semantic, although the ternary thing is just stylistic, so feel free to skip it if you'd prefer … is not None and … is not None
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Honestly I find the ternary expression utterly incomprehensible, but I've corrected this to check for Noneness.
src/twisted/mail/test/test_smtp.py
Outdated
Attempting to connect to an ESMTP server which presents an invalid certificate | ||
should produce a meaningful error. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Love this docstring, very close to https://jml.io/pages/test-docstrings.html .
(It would be even nicer if it gave a crisper definition of "meaningful" but that is really nitpicking)
return self.assertFailure(sentDeferred, smtp.SMTPConnectError).addCallback( | ||
checkVerifyFailedMessage | ||
) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
FYI, this sort of real-networking test that returns a Deferred is really not the right way to test protocol-parsing logic like this; it does unnecessarily non-deterministic I/O with the operating system. If you can, it should be rewritten to be more robust using twisted.test.iosim.IOPump
or similar. At the very least however, please remove the direct call to listenTCP
and instead use the higher-level self.loopback(...)
to at least abstract out the connection-making logic.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah. As I wrote before, I struggled to get this to work with iosim
because startTLS
wants to wrap a real file descriptor.
I had a look at making it work with self.loopback
. It could be my failure to understand how to use loopback
, but as far as I can tell, it wants to build its own ClientFactory
(a LoopbackClientFactory
), which seems like it makes it impossible to instantiate an SMTPSenderFactory
(which is where the interesting changes are here).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You mentioned that there are problems using iosim because startTLS expects a "real life file descriptor"? iosim has built-in support for startTLS though, so I wonder what happened there.
src/twisted/test/test_amp.py tests AMP's startTLS usage and uses iosim
any of the tests that useconnectedServerAndClient
Oops. I see i already mostly gave this review before. I wonder why the robot did not remove the |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hello robot. Please see comment review for requested changes.
You only left a review with an "indecisive comment" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Next time escape the robot command in that comment @adiroiban 😉
please review |
Sadly it's not looking like I'm going to have much time to work on this in the near future. My dayjob has taken me away from regular work on Twisted-based projects, and to be honest I've pretty much forgotten all the state I had on this when I worked on it two years ago. I'd really love it if this could be merged as-is, despite the less-than-ideal test, since IMHO it fixes a real bug that bites users. That said, I do understand I'm not the one that will have to live with the headache of maintaining that test in future. Anyway, in short, if anyone else has time to pick this up and see if the tests can be improved, it would be amazing. |
@richvdh Thanks for clarifying the status here. No worries that you don't have time to volunteer, it is, after all, volunteering :) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think I agree with the reasoning of landing this even with a sub-optimal test. Whoever comes along to land it, please:
- fix up the public name to be private
- clear the exception when it's propagated to a
Deferred
- file a follow-up to fix the test
src/twisted/mail/smtp.py
Outdated
@@ -950,6 +951,9 @@ def __init__(self, identity, logsize=10): | |||
self.code = -1 | |||
self.log = util.LineLog(logsize) | |||
|
|||
# the reason we were unable to successfully send the message, if any. | |||
self.failureReason: Optional[Failure] = None |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This requires an @ivar
if it's going to be made public, but I would suggest making it private, since it is exposed publicly at the relevant time in self.result.errback(...)
below.
exn = SMTPConnectError( | ||
-1, "Unable to connect to SMTP server: " + str(err.value) | ||
) | ||
exn.__cause__ = err.value |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I appreciate the use of __cause__
to chain the exception properly; we should probably make this a bit easier to do with Failure
.
Co-authored-by: Glyph <code@glyph.im>
Scope and purpose
This fixes #10210 by passing the failure reason back to the SMTPSenderFactory out-of-band.
Contributor Checklist:
tox -e lint
to format my patch to meet the Twisted Coding Standard#
character).review
to the keywords field in Trac, and putting a link to this PR in the comment; it shows up in https://twisted.reviews/ now.The first line is automatically generated by GitHub based on PR ID and branch name.
The other lines generated by GitHub should be replaced.