This might be a very niche article, but it's one worth writing. I'm a huge fan of ProtonMail. In fact, I moved all Graphite email to a ProtonMail Pro account last year. I'm attracted to ProtonMail's simple way of obfuscating complex encryption. The whole implementation very much drives a lot of what I have done and what I plan to do with Graphite. That being said, using ProtonMail does have its drawbacks.

There's no built-in calendar client. So, if you're coming from Outlook or Gmail, you're going to be seriously disappointed here. I missed many calls because I was trying to track invites manually. I've (mostly) solved this by simply using Apple Calendar. Another drawback to ProtonMail is its seeming incompatibility with clients that use SMTP settings. But, fortunately, there's a solution to this one.

For paying customers, ProtonMail offers a desktop bridge client. This client silently runs in the background on your machine and will automatically encrypt and decrypt emails, thus allowing you to use desktop email clients, like Apple Mail, for example. But this solution also enables something else.

If you are a developer who wants to use a ProtonMail account along with Nodemailer (the NodeJS-based email sending tool), it would seem at first blush that you can't do it. But, here's where the bridge comes in. As long as you, or users of your application that want to use ProtonMail to send emails through your app, have the desktop bridge running, you can send encrypted emails through ProtonMail from Nodemailer.

Let's take a look at the desktop bridge first. When you download the bridge, you'll be prompted to log in with your ProtonMail credentials. Once you've done so, you'll have a desktop tray app running in your menu bar. From there, you can access the IMAP and SMTP Settings:

This is what you'll need to configure Nodemailer. Let's take a look at a sample configuration and we'll walk through what's going on.

async function main(){
    let transporter = nodemailer.createTransport({
        host: "127.0.0.1",
        port: 1025,
        secure: false, // true for 465, false for other ports
        auth: {
          user: "johnnycash@protonmail.com", 
          pass: "$cHI4cyrtfUM"
        }, 
        tls: {
            rejectUnauthorized: false
        }
      });
  
    let info = await transporter.sendMail({
      from: '"Me" <johnnycash@protonmail.com>',
      to: "someonecool@email.com",
      subject: "Hello!",
      text: "Hello world?",
      html: "<b>Hello world?</b>"
    });
  
    console.log("Message sent: %s", info.messageId);
  }

The above async function handles configuration, but keep in mind it's not a self-calling function. That's a good thing, and you'll see why later. For now, let's look at the config to understand what's going on.

The transporter variable calls a Nodemailer function that takes in your SMTP settings. Pretty straightforward so far. However, there are a couple gotchas to watch out for. The secure property feels like it should be true. I mean, it's ProtonMail, it's pretty damn secure, right? However, the desktop bridge client uses port 1025 for SMTP. The secure property can only be true for port 465. So, make sure you enter false here. Don't worry, your email will still be sent using TLS and encrypted with your ProtonMail key.

The second gotcha is the tls object. When you walk through Nodemailer's documentation, you're not going to see this object in the immediate examples. That's because generally, for hosted email services, TLS certificates are properly signed and won't be rejected. In ProtonMail's case, the bridge client is really just giving you an SMTP layer on your computer. It's handling encryption and decryption in the background and giving you a local server you can use for email clients. But that local server uses a self-signed TLS certificate. This will fail in Nodemailer unless you set that tls object as illustrated above.

The rest should be simple to understand. So, now you're ready to send your email. Using this async function, you can ensure that all promises are returned before the email tries to send. All you need to do now is run the following call from wherever you send emails on your server application: main().catch(console.error);

That's it. Now, you can use ProtonMail yourself with Nodemailer, or you can allow users of your app to configure ProtonMail as their email sender.