Use API Key in Meteor:Email

Hi!

I’ve been using Meteor:Email to send out emails with SendGrid.
This works using simple username:password authentication.

However, SendGrid will soon be requiring clients to use API keys instead for security.

Does anybody have experience using API keys with this lib? I see Email is built off nodemailer and I couldn’t find any option there either. Seems I’d have to use the auth object and then pass in a stringified version of the apiKey as a header.

It would be ideal if I could not make code changes and simply change my MAIL_URL ENV var.

Unfortunately you can’t use SendGrid’s API via Email.send simply by changing your environment variable.

Instead, you need to install the official SendGrid npm module:

npm install --save @sendgrid/mail

Then replace Email.send with your own version:

// somewhere in your server-side code
const sgMail = require('@sendgrid/mail')
sgMail.setApiKey(Meteor.settings.private.sendGrid.apiKey)

Email.send = function(options) {
    // manipulate options to be compatible with sendGrid ??
    sgMail.send(options)
    .then(() => {
        console.log('Email sent')
    })
    .catch((error) => {
        console.error(error)
    })
}

Note that I haven’t personally used SendGrid so I don’t know if the options parameter is 100% compatible, but it looks like it should be. I’ve taken the above straight from their docs.

1 Like

Thanks, I’ve used their api in another project and it’s straightforward.

What would you call your design pattern for overriding Email.send though? Just wanna know the terminology. This will help me a lot in terms of having to replace code in lots of places.

I would call it “lazy but effective”. It could be called monkey patching. Does it need a name?

If the function signatures are identical then you could even just write Email.send = sgMail.send

The alternative is writing your own module and importing that everywhere you use Email.send, then replacing Email.send with your own module. Or, import the sendgrid package everywhere you currently use Email.send and replacing Email.send with sgMail.send, but you have to set the API key somewhere.

1 Like

Hmmm… what about emails sent by the Meteor Accounts package – will that package need an update to continue working with SendGrid?

1 Like

The Accounts packages just use Email.send, so yeah, by monkey patching Email.send the Accounts package emails automatically get routed via SendGrid. I use Mailgun, and that’s exactly why I do it this way.

2 Likes

Figured it be good to know a name so it’d be good to refer to the pattern in future discussions

Another approach would be to use the hookSend where you get the data, send it via the API and return false so the traditional way does not fire.

We should probably improve this.

6 Likes

I like this bc it’s not doing an override of a core fn in the framework and instead using an inherent option of Email (hooks). Only minor caveat is that I’d have to add the hook option to every call now.

1 Like

Any insights on https://en.wikipedia.org/wiki/Monkey_patch#Pitfalls and whether it’s a concern for this use case?

I cannot imagine there’ll be major updates to Email that could break due to the monkey-patch.

oh boy, I’m now finding that calls to Email.send won’t catch an error thrown by sgMail.send in the monkey-patched version. Errors will be logged to the server, but won’t get passed through the chain and get reported to the client.

Maybe I have to wrap all Email.send calls in a try/catch now?

At this point, I think I might as well just unconditionally replace Email.send with sgMail

Never knew about hookSend, nice! I see that it’s not documented yet, except in the v1.11 changelog.

2 Likes

This is because Email.send is expecting to be synchronous (using Fibers), whereas sgMail.send is asynchronous (using promises). The Email.send is returning immediately to the calling function while the promise executes in the background.

In this case you need to make Email.send await the result of the promise. Sorry, my sample code above was probably too simplistic. You can use Meteor.wrapAsync to help with this, but first you need to wrap sgMail.send into a function which calls a function (error, result) {} callback on success or failure, instead of returning a promise. This answer can show you how to do that.

Regarding the ‘pitfalls’ of monkey-patching. I don’t think any of them apply in this case, but the hook would be better if you’re using Meteor 1.11.

1 Like

Thanks for the explanation and links. No need to apologize, sample code should be simplistic. Best

I’m on Meteor 1.8. For some reason, it takes hours to update to 1.11. Any way to just update the Email package?

I tried meteor update email and it said the package was already up to date.

I’m unable to get the error detection working with wrapAsync and converting the Promise to a Callback.

Client Code

 Meteor.call("sendEmail", to, from, cc, subject, function (error, response) {
            if (error) {
                return Bert.alert(error.reason, "danger", "fixed-top");
            } 
           
            Bert.alert("Email Sent", "success", "fixed-top");    
        });

Server Code

//Monkey Patch fn to call sgMail 
Email.send = function(options) {
  
  //Converting promise to use callbacks
  sgMail.send(options)
    .then(result => callback(null, result))
    .catch(error => callback(error));
}
function callback(error, results) {
  if (error) {
    throw error;
  }

  console.log('email sent');
}

const sendEmailSync = Meteor.wrapAsync(Email.send);

//Server helper to send email. It calls wrapped SyncEmail fn above in a try catch
Meteor.methods({
sendEmail: function (to, from, cc, subject) {

    check([to, from, cc, subject], [String]);

    this.unblock();

    try {
      sendEmailSync({
         to: to,
         from: from,
         cc: cc,
         subject: subject,
      });

    } catch (error) {
      throw error;
    }


  });
```

Things seem to be working. However, if I manually force things to break (by uncommenting Email options of to,from,subject,etc...)..The error only gets displayed in the console. It's still not trickling it's way up to the client and displaying on the Bert alert.

Maybe the error is in converting the Promise to a Callback? I tried using `Bluebird's .asCallback` but cannot seem to get that to work in Meteor. 

```
import Promise from 'bluebird';
const promise = sgMail.send(options);
   
 promise.asCallback(function(error, results) {
     if (error) {
       throw error;
     }

     console.log('email sent');
   });
```

But I kept getting `.asCallback` is not a function

That’s not quite the right approach.

// First you need to convert sgMail.send to a callback function:
function sgWithCallback(options, callback) {
    sgMail.send(options)
    .then(result => callback(null, result))
    .catch(error => callback(error));
}

// Then wrap it with wrapAsync:
const sgMailSync = Meteor.wrapAsync(sgWithCallback);

//  Then use the async method in Email.send:
Email.send = function(options) {
    return sgMailSync(options);
    // this will now wait for sgMailSync to resolve or error, so the error can be caught by the parent method and reported to the client.
}

// or just
Email.send = sgMailSync;

// or just:
Email.send = Meteor.wrapAsync(sgWithCallback);

// Then use Email.send as you would normally

Note this code is off-the-top-of-my-head and therefore not tested.

If that doesn’t work, try removing this.unblock() from your method.

2 Likes

Thanks for all your time on this. I just ended up replacing Email.send with sgMail.send. At this point, it seemed to make more sense rather than introducing the monkey-patch & wrapAsync, etc…

Even then, there were a few caveats…SendGrids API required attachments to be a String (had to Base64 encode it). Also, I then had to make my SendEmail Meteor method async and await the sgMail.send call in a try-catch.
(Rob’s now famous blog https://blog.meteor.com/using-promises-and-async-await-in-meteor-8f6f4a04f998)

This finally gave me my desired behavior.

I must be missing something…

According to SendGrid docs (https://sendgrid.com/docs/for-developers/sending-email/integrating-with-the-smtp-api/) you should not need to do anything else, but replace the username with ‘apikey’ and use the value of the Send Grid API Key as the password.

2 Likes

Yes, this would’ve been the amazing 10 min non-code related fix I wanted.
However, it resulted in me getting a 535 error for bad username/password.

Couldn’t find anything more on why this was. I’d like to be able to call their support team but couldn’t find anything. I wish they didn’t alert their users about this change only 30 days prior.