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.
// 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.
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.
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.
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.
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
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.
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
// 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.
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)
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.