Hello,
First an introduction: I am one of the primary developers of
Thunderbird, and I am also the person behind jsmime (cf.
<
https://github.com/mozilla-comm/jsmime>, although I have a lot of
updates to that code present only in my fork since I'm still working on
them).
Recently, I have started looking into moving larger portions of our
codebase into JavaScript for easier maintenance. It makes a good deal
sense to share and reuse open-source JS libraries for core protocol
aspects (e.g., IMAP, POP, SMTP, NNTP, MIME). As SMTP is the simplest of
these protocols and the most isolated in our codebase, as well as
something I'm intending to modify anyways in the near future to
introduce EAI support, I've started by taking a look at smtpclient.js to
see how much work it would take to be possible to use in Thunderbird.
Since I've not communicated too much in the past with the primary
maintainers of these projects, I don't know what the willingness would
be to, for example, adopt newer ES6 features (such as generators and
promises, see later) or make changes to the API. I'm aware that the
SpiderMonkey engine and Gecko DOM APIs are far more aggressive in
picking up new features than Node.js, so I personally have a strong bias
towards using these new features as they are available and I'm aware of
the impact that causes for attempting to use libraries as web client
pages or node.js.
What follows is a list of major issues I see in the present codebase
based on the current implementation, roughly ordered in decreasing
severity. I've not tried running it in detail or doing any porting work
to see what works and what breaks.
1. smtpclient.js appears to assume that messages will always be valid
UTF-8 strings when encoded. Unfortunately, this is most definitely not
the case. My current designs for JSMime involve potentially creating
non-UTF-8 8-bit parts for attachments if they can be legally encoded as
such. I've also been repeatedly informed by the Japanese localization
team that supporting ISO-2022-JP as the charset body is a non-negotiable
requirement [1], so I get to have to support encoding strings to
non-UTF-8 charsets as much as I hate doing so. :-(
2. There is very little support for different authentication mechanisms.
Thunderbird uses effectively a tiered auth system: we deduce from prefs
the set of acceptable authentication methods, compare with that the list
of mechanisms that the server supports and tries them in turn. The
source code can be seen at
<
https://dxr.mozilla.org/comm-central/source/mailnews/compose/src/nsSmtpProtocol.cpp#873>
(warning: this isn't a permalink, so it may cease to be valid in a few
months), if you're willing to dig into strange C++ code.
Supporting CRAM-MD5 is necessary to pass tests, and I suspect that
dropping support for NTLM or GSSAPI is untenable. At the same time, I
also recognize that asking for a JS implementation of NTLM or GSSAPI is
completely unworkable. Given the architecture of how SASL works (it's
effectively a middleman between authentication protocols and other
protocols), and its utility as well for the other protocols, it's
plausible that we could permit an interactive SASL session
authentication mechanism to handle this mechanism. I'll also note that,
judging from bug activity, it's unlikely that we would need to support
the data security layer.
2a. Another minor difference in authentication is that Thunderbird
lazily processes the authentication when it's done at the protocol,
instead of providing the parameters when necessary. Only for NNTP is
there a benefit to doing it Thunderbird's way, but I don't think it's
worth adversely impacting the API for current clients for the sake of
symmetry for a protocol most clients don't care about. Hence why this is
a separate sub-point.
3. smtpclient.js does not support the delivery status notification
extension.
4. The debug logging infrastructure seems underwhelming. Our current
implementation saves to a log all of the traffic that is sent and
received, merely blanking out the authentication steps (see
<
https://dxr.mozilla.org/comm-central/source/mailnews/compose/src/nsSmtpProtocol.cpp#1569>).
I am aware that some potential users may be concerned about that level
of debugging knowledge, but having had to resort to following these logs
before, I believe there is benefit in these steps that requires logging
this much data.
5. There appears to be no provisioning for reporting EHLO capabilities.
These are useful to, for example, know which authentication methods may
be attempted, or whether or not the server is EAI-compliant, or if DSN
is supported. I would suggest utility of 8BITMIME and BINARYMIME, but
just-send-8 has been the prevailing view among MTAs, and I'm still wary
of using the Content-Transfer-Encoding: binary encoding.
6. There seems to be a strong footgun effect if you attempt to use EAI
or IDN addresses on a non-EAI server: it looks like the client is purely
responsible for applying UTR46 (or IDNA2003/2008) processing to IDN
hostnames in EHLO or constituent pieces of email addresses.
The following points are issues of design that are purely my own opinions:
7. Having used Promises a lot, I've come to view event-based
notification systems as increasingly cumbersome and problematic to use
[2]. For example, take the simplicity of this API:
client.send({from: "
some...@example.com", to: ["
ot...@example.com"]},
stream).then(
() => console.log("Message sent!"),
(e) => console.log("Message failed: " + err)
);
8. If you combine promises and generators, you can build a framework
that very easily supports the line-oriented, semi-UTF-8 nature of SMTP,
POP, and NNTP. For example, a message generator might look like:
function* sendMessage(conn, to, from, msg) {
let [code, text] = yield conn.runCommand("MAIL FROM:<" + from + ">");
if (code < 250 || code > 259)
throw new Error("Your SMTP server won't let you send");
for (let rcpt of from) {
[code, text] = yield conn.runCommand("RCPT TO:<" + rcpt + ">");
if (code != 250)
throw new Error("Not doing error handling in this example");
}
let [code, text] = yield conn.runCommand("DATA");
if (code != 354)
throw new Error("Still not doing this");
yield conn.sendMultilineStream(msg);
}
[Side note: this example is adapted from an experimental NNTP client I
wrote, since I figure people here might understand SMTP better than
NNTP. The client used a small connection pool, which influenced how I
did some of the API.]
This kind of implementation strategy I personally find to be much easier
to read, but it also absolutely requires generators and nearly
absolutely requires promises. You get the clarity of imperative-style
code (including proper exception handling) but still get the
asynchronous I/O processing that is desired.
I hope that I am not coming off as someone who is saying "your
implementation sucks; let me design a new one for you" (although I do
feel strongly about the utility of Promises, so some amount of
evangelism there was intentional). I think it would be a shame not to
settle on one high-quality source of libraries for email utilities, and
the current implementation appears to be a good start. It's just that
Thunderbird demands more features in its SMTP code than the current
library is supplying.
Thoughts/questions/comments/concerns/flames/diatribes/rants?
[1] It gets worse: the jp team would rather mangle text bodies than
silently fallback from ISO-2022-JP to UTF-8. As I've said in prior
communications about jp email, "The logic that the JP locale wants is
incompatible with good logic for the rest of the world."
[2] The newer drafts of TCPSocket specify a Streams-based API for
working with the stream instead of the event-based Mozilla variant. The
first thing I did when playing with TCPSocket was create a
quick-and-dirty wrapper to give me something akin to the stream-based
API, because I preferred that version so much. Unfortunately, it does
not look like Mozilla (or anyone else?) is looking to implement the
streams-based version anytime soon.
--
Joshua Cranmer
Thunderbird and DXR developer
Source code archĂŚologist
--
Joshua Cranmer
Thunderbird and DXR developer
Source code archĂŚologist