Skip to content

Conversation

@alex-robbins
Copy link
Owner

This pull request is for internal review by coworkers. It is a precursor to an upstream pull request.

Caveats with this branch currently:

Note that I will force push this branch, because that's how upstream wants to do it (in accordance with their contributor guidlines). When I do, I'll leave a message here saying so. If this makes anyone uncomfortable, let me know.


This pull request adds an implementation of the smtplib module for micropython-smtplib.

Following the recommendations of the contributor guidelines, I took the original source from CPython 3.3.6, as linked to in the first commit's message. I then looked at the diff between that and CPython 3.6, and backported the changes that didn't appear to add bloat (mostly bug fixes). This is not expressly called for by the contributor guidelines, and could be considered to go against the directive to minimize the size of the diff from upstream. However, it reduces the diff from CPython 3.6, and also includes several bug fixes. It didn't seem wise to ignore existing fixes for bugs that come without substantial increase in the size of the code. These commits are "Close before raising" through "Defer SMTPServerDisconnected during RSET". If you really want to stay close to 3.3 and ignore those bugs, we can drop these commits.

I also removed a lot of dependencies, for example by:

  • using string functions instead of regular expressions (which frankly were overkill even on CPython),
  • using ubinascii directly instead of pulling in the base64 and email.base64mime modules and everything that comes with them, and
  • Moving some imports to within the send_message function, so that the rest of the library can be used without them.

Other changes should have an obvious reason for being there, or should be justified in the commit message. Let me know if anything requires further explanation.

I didn't write any automated tests because it is not obvious to me how to involve external stuff like the SMTP server. The code from upstream that shows example usage if __name__ == '__main__' can still be used.

This code has been tested on Unix and the ESP8266 (mine didn't have enough memory to compile; this and a couple dependencies needed to be frozen in). Fun fact: It actually remains compatible with CPython if you revert "Prefer self.file over self.sock" and remove the u-prefixes from module names.

Thanks,
Alex

@alex-robbins
Copy link
Owner Author

Note that when I say "Commits 'x' through 'y', I mean topologically, as shown by git log, not by date, as GitHub shows them.

Copy link
Collaborator

@DrASK DrASK left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good to me. Seems like a simple enough port.

From CPython 3.6, to prevent leaking resources
From CPython 3.6. Also, socket.error is not always available in
MicroPython.
MicroPython doesn't have socket.getfqdn or socket.gethostname.
These modules will not be needed unless send_message is called. This is
particularly useful given that email.generator is not yet in
micropython-lib.
On MicroPython, calling sys.stdout.flush() raises EINVAL.
All uses of the re module have been removed.
This eliminates the dependencies on the base64 and email.base64mime
modules.
str.partition is not available by default on all ports.
As discussed in micropython/micropython#1375, the Unix port's
usocket.connect method takes a bytes object that represents a
struct sockaddr. In the case of Unix domain sockets, it's a
struct sockaddr_un. There's no portable way to pack that struct (since
it can be defined differently on different platforms), so we'll let the
caller handle it. On many Linux platforms, this can be achieved with
something like ustruct.pack('H108s', usocket.AF_UNIX, '/path/to/sock').
Done by reimplementing _GLOBAL_DEFAULT_TIMEOUT and create_connection.
According to the documentation, usocket.socket.sendall has undefined
behavior on non-blocking sockets. Furthermore, the method does not
even appear to exist in the Unix port (documentation notwithstanding).

Instead, we'll wrap send in a loop.
The result of MicroPython's ussl.wrap_socket does not have the send or
makefile methods, unlike CPython. Because of the possibility that
starttls has been called (not to mention the possibility that SSL was
used from the outset, as in the SMTP_SSL class), it is not generally
known whether self.sock is an SSL-wrapped socket or not. As such, we
cannot, in general, call self.sock.makefile or self.sock.send.

Instead of potentially calling self.sock.makefile in getreply (as is
done upstream), I've opted to create self.file exactly when creating
self.sock (no more or less). This works because, when creating
self.sock, it is always known whether self.sock is SSL-wrapped or not,
and thus it is known how to get a self.file.

An alternative solution (for MicroPython) would be to never call
self.sock.makefile, and take advantage of the fact that MicroPython
sockets have read and write methods already (using write instead of
send). That seemed less desirable though, since it would potentially
make merging of upstream patches difficult, while providing no clear
benefit. (The two options seemed like a wash in terms of minimizing the
size of the diff from upstream.)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants