Lessons Learned Working on Enigmail
For the last seven weeks, a group of three Thoughtworks developers have worked on improving the open source tool Enigmail . Doing this was a very interesting experience - we learnt a lot about Thunderbird, JavaScript, GPG and encryption. As we are writing this, we are winding down work on Enigmail and will soon pick up another open source project.
In this article we cover what we were trying to achieve, what we managed to do and some of the challenges along the way.
What is Enigmail?
Enigmail is a plugin for the mail client Thunderbird. It allows you to read and write encrypted and signed emails from inside of Thunderbird; it’s currently the most popular way of using OpenPGP (the most popular standard for email encryption). The project is free software and was started in 2001. Since then it has grown and acquired a lot of functionality. Not only does Enigmail allow you to decrypt, encrypt, sign and verify emails - it also supports management of encryption keys from inside of Thunderbird.
Most of Enigmail is written in a dialect of the JavaScript language. It contains 82 JavaScript files, but much of the JavaScript is also embedded in other file types. According to cloc (a tool for measuring lines of code), there are about 30,000 lines of actual code in those files.
Why Enigmail?
Before talking about what we actually did, we should talk briefly about why the team chose to work on Enigmail. Our team was created to work specifically on issues related to online privacy, anonymity and security. Among all open source projects in that field, we felt that encrypted email is probably one of the most common forms of private communication currently in use. At the same time, many of the projects out there are suffering from working in an environment with very scarce resources while trying to support high risk people around the world.
Enigmail is an extremely important project, and one that has suffered from some technical debt. Keep in mind that the project has been around for almost 15 years and is supporting a large number of environments and weird idiosyncrasies in tools and operating systems. On top of that, the community supporting Enigmail is quite small and there are no full-time developers on the project.
These things together made the decision quite easy:
We decided that our contributions to Enigmail would be focused on making the project easier to work with so that more people could easily contribute, and to make it much easier to identify the causes for bugs.
Thunderbird Extensions
Since Enigmail is an extension to Thunderbird, it only runs inside of that environment. Thunderbird extensions can be quite complicated and there are a large number of APIs and technologies that can be used to write them. The most common variation of extension is written in JavaScript, but it’s also possible to write them using C++ and other languages. In Firefox and Thunderbird, this is made possible by a component framework called XPCOM.
In order to write functionality that interacts with the user interface of Thunderbird, you have to write files in a format called XUL. This is a format similar to HTML that can be manipulated from JavaScript. In addition, to achieve various tasks Thunderbird exposes a large number of components that can be used from inside of JavaScript; everything from parsing email messages to doing manipulation of files on disk can be done through these components. Since there are a lot of them, it is sometimes hard to know how to achieve a specific task. Figuring out which component to use can be a significant time sink.
Because of all these features, it is not possible to run Thunderbird extension JavaScript outside of Thunderbird. Almost all JavaScript code for extensions are full of references to Thunderbird-specific objects and components. That means using node.js or any of the other tools out there for testing is not possible.
Finally, the Thunderbird developers are also quite aggressive in adopting new JavaScript features - sometimes before they become standards. This means that the JavaScript in Thunderbird isn't always completely compatible with regular JavaScript. But it also means that nice new features are freely available to use (this is true for Firefox and other Mozilla tools as well) - things like comprehensions and lexically scoped variables can make a lot of difference for readability.
In order to run code from an extension you have to compile it into a file format called XPI, and then load it inside Thunderbird. That makes the development cycle a bit longer than we were used to from other types of projects - we can't just make a one-line change and see the result in our tests in less than a second. That said, the compilation process for Enigmail is quite fast so we still got pretty rapid feedback.
What Did we Do?
With the purpose of making Enigmail easier to contribute to, our focus was mostly on things that won't be visible to end users. We did end up achieving quite a lot in the time working on this project.
Provisioning
In order to make it easy to contribute, the first step is to make it extremely easy to get the initial environment up and running so you can write code. There are not that many steps involved in doing it manually, but we are fans of automation, so we decided to make the process of getting an environment automated. We ended up doing this with both Vagrant and Docker - that means you can always have a repeatable environment to do development in. We ended up providing support for both to make it possible for people from different environments to get going quickly - Vagrant works better on OS X, while Docker gives a much nicer development experience on Linux. Assuming you have Vagrant or Docker installed, you only need one command in a checked out repository to have a running environment.
Continuous Integration
After we had figured out what needed to happen in order to provision new machines from scratch for the project, we decided that we wanted a way to automatically know when we broke the build. We achieved this by hooking Travis CI to our repository. With that in place, every time we push a new commit, all tests will be run against a new environment and we will be notified if something fails.
Most of the time, this worked really well - but there were also moments where the Travis environment was different enough that it caused weird issues for us. Probably the worst of those were based on strange MIME settings in the Travis environment. But overall, this provides for a very easy path to keep our code tested and running. It also means that if someone clones the repository they can get continuous integration setup for their repository very easily.
Testing
When we first started working on the project, there were almost no tests in the repository. Since we believe automated testing is a good way of keeping track of the quality of a project, we decided early on that adding both unit tests and larger functional tests should be a priority for us.
Sadly, testing is not so easy to do inside of Thunderbird, for the reasons mentioned above - but the developers of Enigmail had helpfully created a small test framework to make it possible (JSUnit). We ended up using this framework for building tests, and we currently have around 150 tests. There are still large parts of the system that are untested, but the situation is better now than it was - and our hope is that new contributors can start out by adding to the tests.
There is another tool called mozmill, that provides for a way to run UI-level tests. However, we didn’t have time to investigate this further.
Refactoring
Once we had some tests to make us feel comfortable, we started out doing refactoring. A large part of these refactorings were focused on breaking out functionality into smaller components that could be understood and worked with independently of each other. In the beginning, almost all Enigmail functionality could be found in only a few modules - EnigmailCommon, EnigmailCore, Enigmail, EnigmailFuncs, EnigmailKeyMgmt and EnigmailDecryptPermanently. Together these modules were responsible for almost 10,000 lines of code. A lot of the functionality in EnigmailCommon, EnigmailFuncs, Enigmail and EnigmailCore was also in the form of different kinds of utilities that didn't really have that much in common with each other.
After we finished our refactorings we are now in a state where there is a much larger number of independent modules - before our refactorings there were 16 main modules, and afterwards 55. We ended up breaking out functionality like encryption, decryption and hashing into different modules. Interacting with GPG and interacting with the GPG agent also got their own modules. We added modules for dealing with logging, external execution and error handling, and so on. EnigmailCommon is completely gone, while EnigmailCore, Enigmail and EnigmailFuncs are minimal. Our hope is that these changes will make it easier for contributors to get an overview of the project and start working in different areas of the code.
We also made smaller changes all over the code base, such as changing var-declarations into const-declarations where they made sense.
Static Analysis
For a project like Enigmail, that has existed for many years, code style has a tendency to suffer. The source for Enigmail had vastly different code styles and also in some places issues that are considered problematic in modern JavaScript. In order to work through these issues we ran JSHint against the full code base - it turns out JSHint has support for the type of JavaScript that runs in Mozilla projects. This gave us roughly one thousand warnings that we proceeded to fix. Most of these were issues along the lines of missing semicolons, usages of "new Object()" instead of {}, and so on - but we also found more serious issues this way, such as unintended fall-throughs in switch statements.
After getting the JSHint issues sorted, we proceeded to make most of the source files compliant with strict JavaScript. This helped us identify even more areas with potential issues.
Our hope is that going forward, the Enigmail project can keep running static analysis tools to make sure the code stays as clean as possible. Following these kind of recommendations make it easier to avoid simple mistakes that cause tricky bugs.
You might ask whether we have any data on our test coverage. We tried figuring out how to get a solution for this working, but it seems that test coverage inside of Thunderbird is an unsolved problem. After we looked at the problem for a while we decided to move on and contribute in other ways instead - but we still would love to see this problem solved. We’re sure there are many Firefox and Thunderbird extensions out there for whom having a way of checking test coverage would be extremely valuable.
Conclusion
At the end of this stint, we feel we have achieved what we wanted to achieve. The code base feels like it's in better shape now - and hopefully that will translate into more contributions and an easier time building new features. I know I will continue hacking on Enigmail in my spare time.
Much thanks to Patrick Brunschwig and Nico Josuttis of the Enigmail team for their help and patience with us. Also thanks to Rosalie Tolentino who provided some very needed testing and documentation of our provisioning scripts.
The team from Thoughtworks is currently composed of Fan Jiang, Iván Pazmiño, Reinaldo de Souza and Ola Bini.
Disclaimer: The statements and opinions expressed in this article are those of the author(s) and do not necessarily reflect the positions of Thoughtworks.