From f6eea4b228672f617082f2136093189cc7605e9f Mon Sep 17 00:00:00 2001 From: filipenevola Date: Sat, 17 Dec 2022 01:34:33 -0400 Subject: [PATCH 1/7] Adds a comment to wrapAsync usage inside deprecated http package --- packages/deprecated/http/httpcall_server.js | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/deprecated/http/httpcall_server.js b/packages/deprecated/http/httpcall_server.js index 0413d0bdba..9ed4549c5f 100644 --- a/packages/deprecated/http/httpcall_server.js +++ b/packages/deprecated/http/httpcall_server.js @@ -161,4 +161,5 @@ function _call (method, url, options, callback) { .catch(err => callback(err)); } +// we are keeping wrapAsync here as this package is deprecated HTTP.call = Meteor.wrapAsync(_call); From d631371311c059e774f483554300574f454a6d8e Mon Sep 17 00:00:00 2001 From: filipenevola Date: Sat, 17 Dec 2022 01:39:34 -0400 Subject: [PATCH 2/7] Changes email package to be fully async Removes email package sync tests Fixes email package test when a hook should cancel the send action --- docs/history.md | 22 +++++++ packages/email/email.js | 81 +++--------------------- packages/email/email_tests.js | 116 ++-------------------------------- 3 files changed, 37 insertions(+), 182 deletions(-) diff --git a/docs/history.md b/docs/history.md index 2c41408d65..729b5b4e61 100644 --- a/docs/history.md +++ b/docs/history.md @@ -1,3 +1,25 @@ +## v3.0, TBD + +### Highlights + +#### Breaking Changes + +* `email`: + `Email.send` is no longer available. Use `Email.sendAsync` instead. + +#### Internal API changes + + +#### Migration Steps + +You can follow in [here](https://guide.meteor.com/3.0-migration.html). + +#### Meteor Version Release + +#### Special thanks to + +For making this great framework even better! + ## v2.9, 2022-12-12 ### Highlights diff --git a/packages/email/email.js b/packages/email/email.js index eed8dbd9b3..dabfbe4122 100644 --- a/packages/email/email.js +++ b/packages/email/email.js @@ -24,7 +24,7 @@ export const EmailInternals = { const MailComposer = EmailInternals.NpmModules.mailcomposer.module; -const makeTransport = function (mailUrlString) { +const makeTransport = async function (mailUrlString) { const mailUrl = new URL(mailUrlString); if (mailUrl.protocol !== 'smtp:' && mailUrl.protocol !== 'smtps:') { @@ -52,10 +52,7 @@ const makeTransport = function (mailUrlString) { mailUrl.query.pool = 'true'; } - const transport = nodemailer.createTransport(url.format(mailUrl)); - - transport._syncSendMail = Meteor.wrapAsync(transport.sendMail, transport); - return transport; + return nodemailer.createTransport(url.format(mailUrl)); }; // More info: https://nodemailer.com/smtp/well-known/ @@ -96,20 +93,17 @@ const knownHostsTransport = function (settings = undefined, url = undefined) { ); } - const transport = nodemailer.createTransport({ + return nodemailer.createTransport({ service: settings?.service || service, auth: { user: settings?.user || user, pass: settings?.password || password, }, }); - - transport._syncSendMail = Meteor.wrapAsync(transport.sendMail, transport); - return transport; }; EmailTest.knowHostsTransport = knownHostsTransport; -const getTransport = function () { +const getTransport = async function () { const packageSettings = Meteor.settings.packages?.email || {}; // We delay this check until the first call to Email.send, in case someone // set process.env.MAIL_URL in startup code. Then we store in a cache until @@ -130,7 +124,7 @@ const getTransport = function () { this.cache = knownHostsTransport(packageSettings, url); } else { this.cacheKey = url; - this.cache = url ? makeTransport(url, packageSettings) : null; + this.cache = url ? await makeTransport(url, packageSettings) : null; } } return this.cache; @@ -170,10 +164,6 @@ const devModeSendAsync = function (mail, options) { }); }; -const smtpSend = function (transport, mail) { - transport._syncSendMail(mail); -}; - const sendHooks = new Hook(); /** @@ -198,58 +188,6 @@ Email.hookSend = function (f) { */ Email.customTransport = undefined; -/** - * @summary Send an email. Throws an `Error` on failure to contact mail server - * or if mail server returns an error. All fields should match - * [RFC5322](http://tools.ietf.org/html/rfc5322) specification. - * - * If the `MAIL_URL` environment variable is set, actually sends the email. - * Otherwise, prints the contents of the email to standard out. - * - * Note that this package is based on **nodemailer**, so make sure to refer to - * [the documentation](http://nodemailer.com/) - * when using the `attachments` or `mailComposer` options. - * - * @locus Server - * @param {Object} options - * @param {String} [options.from] "From:" address (required) - * @param {String|String[]} options.to,cc,bcc,replyTo - * "To:", "Cc:", "Bcc:", and "Reply-To:" addresses - * @param {String} [options.inReplyTo] Message-ID this message is replying to - * @param {String|String[]} [options.references] Array (or space-separated string) of Message-IDs to refer to - * @param {String} [options.messageId] Message-ID for this message; otherwise, will be set to a random value - * @param {String} [options.subject] "Subject:" line - * @param {String} [options.text|html] Mail body (in plain text and/or HTML) - * @param {String} [options.watchHtml] Mail body in HTML specific for Apple Watch - * @param {String} [options.icalEvent] iCalendar event attachment - * @param {Object} [options.headers] Dictionary of custom headers - e.g. `{ "header name": "header value" }`. To set an object under a header name, use `JSON.stringify` - e.g. `{ "header name": JSON.stringify({ tracking: { level: 'full' } }) }`. - * @param {Object[]} [options.attachments] Array of attachment objects, as - * described in the [nodemailer documentation](https://nodemailer.com/message/attachments/). - * @param {MailComposer} [options.mailComposer] A [MailComposer](https://nodemailer.com/extras/mailcomposer/#e-mail-message-fields) - * object representing the message to be sent. Overrides all other options. - * You can create a `MailComposer` object via - * `new EmailInternals.NpmModules.mailcomposer.module`. - */ -Email.send = function (options) { - if (Email.customTransport) { - // Preserve current behavior - const email = options.mailComposer ? options.mailComposer.mail : options; - let send = true; - sendHooks.forEach((hook) => { - send = hook(email); - return send; - }); - if (!send) { - return; - } - const packageSettings = Meteor.settings.packages?.email || {}; - Email.customTransport({ packageSettings, ...email }); - return; - } - // Using Fibers Promise.await - return Promise.await(Email.sendAsync(options)); -}; - /** * @summary Send an email with asyncronous method. Capture Throws an `Error` on failure to contact mail server * or if mail server returns an error. All fields should match @@ -284,12 +222,11 @@ Email.send = function (options) { * `new EmailInternals.NpmModules.mailcomposer.module`. */ Email.sendAsync = async function (options) { - const email = options.mailComposer ? options.mailComposer.mail : options; let send = true; - sendHooks.forEach((hook) => { - send = hook(email); + await sendHooks.forEachAsync(async (sendHook) => { + send = await sendHook(email); return send; }); if (!send) { @@ -313,8 +250,8 @@ Email.sendAsync = async function (options) { } if (mailUrlEnv || mailUrlSettings) { - const transport = getTransport(); - smtpSend(transport, email); + const transport = await getTransport(); + await transport.sendMail(email); return; } return devModeSendAsync(email, options); diff --git a/packages/email/email_tests.js b/packages/email/email_tests.js index 6f016f26b9..585d4b9990 100644 --- a/packages/email/email_tests.js +++ b/packages/email/email_tests.js @@ -10,22 +10,9 @@ const sleep = (ms) => { return new Promise((resolve) => setTimeout(resolve, ms)); }; -// Create dynamic sync tests -TEST_CASES.forEach(({ title, options, testCalls }) => { - Tinytest.add(`[Sync] ${title}`, function (test) { - smokeEmailTest((stream) => { - Object.entries(options).forEach(([key, option]) => { - const testCall = testCalls[key]; - Email.send({ ...option, stream }); - testCall(test, stream); - }); - }); - }); -}); - // Create dynamic async tests TEST_CASES.forEach(({ title, options, testCalls }) => { - Tinytest.addAsync(`[Async] ${title}`, function (test, onComplete) { + Tinytest.addAsync(`${title}`, function (test, onComplete) { smokeEmailTest((stream) => { const allPromises = Object.entries(options).map(([key, option]) => { const testCall = testCalls[key]; @@ -38,82 +25,10 @@ TEST_CASES.forEach(({ title, options, testCalls }) => { }); }); -// Individual sync tests - -Tinytest.add( - '[Sync] email - alternate API is used for sending gets data', - function (test) { - smokeEmailTest(function (stream) { - Email.customTransport = (options) => { - test.equal(options.from, 'foo@example.com'); - }; - Email.send({ - from: 'foo@example.com', - to: 'bar@example.com', - text: '*Cool*, man', - html: 'Cool, man', - stream, - }); - test.equal(stream.getContentsAsString('utf8'), false); - }); - - smokeEmailTest(function (stream) { - Meteor.settings.packages = CUSTOM_TRANSPORT_SETTINGS; - Email.customTransport = (options) => { - test.equal(options.from, 'foo@example.com'); - test.equal(options.packageSettings?.service, '1on1'); - }; - - Email.send({ - from: 'foo@example.com', - to: 'bar@example.com', - text: '*Cool*, man', - html: 'Cool, man', - stream, - }); - - test.equal(stream.getContentsAsString('utf8'), false); - }); - Email.customTransport = undefined; - Meteor.settings.packages = undefined; - } -); - -Tinytest.add('[Sync] email - hooks stop the sending', function (test) { - // Register hooks - const hook1 = Email.hookSend((options) => { - // Test that we get options through - test.equal(options.from, 'foo@example.com'); - console.log('EXECUTE'); - return true; - }); - const hook2 = Email.hookSend(() => { - console.log('STOP'); - return false; - }); - const hook3 = Email.hookSend(() => { - console.log('FAIL'); - }); - smokeEmailTest(function (stream) { - Email.send({ - from: 'foo@example.com', - to: 'bar@example.com', - text: '*Cool*, man', - html: 'Cool, man', - stream, - }); - - test.equal(stream.getContentsAsString('utf8'), false); - }); - hook1.stop(); - hook2.stop(); - hook3.stop(); -}); - // Individual Async tests Tinytest.addAsync( - '[Async] email - alternate API is used for sending gets data', + 'email - alternate API is used for sending gets data', function (test, onComplete) { const allPromises = []; smokeEmailTest((stream) => { @@ -161,7 +76,7 @@ Tinytest.addAsync( ); Tinytest.addAsync( - '[Async] email - hooks stop the sending', + 'email - hooks stop the sending', function (test, onComplete) { // Register hooks const hook1 = Email.hookSend((options) => { @@ -197,7 +112,7 @@ Tinytest.addAsync( // Another tests -Tinytest.add('[Sync] email - URL string for known hosts', function (test) { +Tinytest.add('email - URL string for known hosts', function (test) { const oneTransport = EmailTest.knowHostsTransport({ service: '1und1', user: 'test', @@ -254,7 +169,7 @@ Tinytest.add('[Sync] email - URL string for known hosts', function (test) { }); Tinytest.addAsync( - '[Async] email - with custom transport exception', + 'email - with custom transport exception', async function (test) { Meteor.settings.packages = CUSTOM_TRANSPORT_SETTINGS; Email.customTransport = (options) => { @@ -274,7 +189,7 @@ Tinytest.addAsync( ); Tinytest.addAsync( - '[Async] email - with custom transport long time running', + 'email - with custom transport long time running', async function (test) { Meteor.settings.packages = CUSTOM_TRANSPORT_SETTINGS; Email.customTransport = async (options) => { @@ -290,22 +205,3 @@ Tinytest.addAsync( Email.customTransport = undefined; } ); - -Tinytest.addAsync( - '[Sync] email - with custom transport long time running', - function (test, onComplete) { - Meteor.settings.packages = CUSTOM_TRANSPORT_SETTINGS; - Email.customTransport = async (options) => { - await sleep(3000); - test.equal(options.from, 'foo@example.com'); - test.equal(options.packageSettings?.service, '1on1'); - Meteor.settings.packages = undefined; - Email.customTransport = undefined; - onComplete(); - }; - Email.send({ - from: 'foo@example.com', - to: 'bar@example.com', - }); - } -); From 222e801235f5e0a34eff462f626dbcade4705e47 Mon Sep 17 00:00:00 2001 From: filipenevola Date: Sat, 17 Dec 2022 07:57:56 -0400 Subject: [PATCH 3/7] Adds Email.send using Email.sendAsync inside, handles error and prints a warning in case of success --- packages/email/email.js | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/packages/email/email.js b/packages/email/email.js index dabfbe4122..bbdf0dab39 100644 --- a/packages/email/email.js +++ b/packages/email/email.js @@ -256,3 +256,22 @@ Email.sendAsync = async function (options) { } return devModeSendAsync(email, options); }; + +/** + * @deprecated use Email.sendAsync + * @param options + */ +Email.send = function(options) { + Email.sendAsync(options) + .then(() => + console.warn( + `Email.send is no longer recommended, you should use Email.sendAsync` + ) + ) + .catch(e => + console.error( + `Email.send is no longer recommended and an error happened`, + e + ) + ); +}; From 7f4f9e3efd7f7d89bfe60eee42d547da821ddd56 Mon Sep 17 00:00:00 2001 From: filipenevola Date: Sat, 17 Dec 2022 08:11:43 -0400 Subject: [PATCH 4/7] Changes `Accounts.sendResetPasswordEmail`, `Accounts.sendEnrollmentEmail`,`Accounts.sendVerificationEmail` to async --- packages/accounts-password/password_server.js | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/packages/accounts-password/password_server.js b/packages/accounts-password/password_server.js index 5127342209..6b6a8d59f6 100644 --- a/packages/accounts-password/password_server.js +++ b/packages/accounts-password/password_server.js @@ -368,7 +368,7 @@ const pluckAddresses = (emails = []) => emails.map(email => email.address); // Method called by a user to request a password reset email. This is // the start of the reset process. -Meteor.methods({forgotPassword: options => { +Meteor.methods({forgotPassword: async options => { check(options, {email: String}) const user = Accounts.findUserByEmail(options.email, { fields: { emails: 1 } }); @@ -382,7 +382,7 @@ Meteor.methods({forgotPassword: options => { email => email.toLowerCase() === options.email.toLowerCase() ); - Accounts.sendResetPasswordEmail(user._id, caseSensitiveEmail); + await Accounts.sendResetPasswordEmail(user._id, caseSensitiveEmail); }}); /** @@ -532,12 +532,12 @@ Accounts.generateVerificationToken = (userId, email, extraTokenData) => { * @returns {Object} Object with {email, user, token, url, options} values. * @importFromPackage accounts-base */ -Accounts.sendResetPasswordEmail = (userId, email, extraTokenData, extraParams) => { +Accounts.sendResetPasswordEmail = async (userId, email, extraTokenData, extraParams) => { const {email: realEmail, user, token} = Accounts.generateResetToken(userId, email, 'resetPassword', extraTokenData); const url = Accounts.urls.resetPassword(token, extraParams); const options = Accounts.generateOptionsForEmail(realEmail, user, url, 'resetPassword'); - Email.send(options); + await Email.sendAsync(options); if (Meteor.isDevelopment) { console.log(`\nReset password URL: ${url}`); } @@ -562,12 +562,12 @@ Accounts.sendResetPasswordEmail = (userId, email, extraTokenData, extraParams) = * @returns {Object} Object with {email, user, token, url, options} values. * @importFromPackage accounts-base */ -Accounts.sendEnrollmentEmail = (userId, email, extraTokenData, extraParams) => { +Accounts.sendEnrollmentEmail = async (userId, email, extraTokenData, extraParams) => { const {email: realEmail, user, token} = Accounts.generateResetToken(userId, email, 'enrollAccount', extraTokenData); const url = Accounts.urls.enrollAccount(token, extraParams); const options = Accounts.generateOptionsForEmail(realEmail, user, url, 'enrollAccount'); - Email.send(options); + await Email.sendAsync(options); if (Meteor.isDevelopment) { console.log(`\nEnrollment email URL: ${url}`); } @@ -711,7 +711,7 @@ Meteor.methods({resetPassword: async function (...args) { * @returns {Object} Object with {email, user, token, url, options} values. * @importFromPackage accounts-base */ -Accounts.sendVerificationEmail = (userId, email, extraTokenData, extraParams) => { +Accounts.sendVerificationEmail = async (userId, email, extraTokenData, extraParams) => { // XXX Also generate a link using which someone can delete this // account if they own said address but weren't those who created // this account. @@ -720,7 +720,7 @@ Accounts.sendVerificationEmail = (userId, email, extraTokenData, extraParams) => Accounts.generateVerificationToken(userId, email, extraTokenData); const url = Accounts.urls.verifyEmail(token, extraParams); const options = Accounts.generateOptionsForEmail(realEmail, user, url, 'verifyEmail'); - Email.send(options); + await Email.sendAsync(options); if (Meteor.isDevelopment) { console.log(`\nVerification email URL: ${url}`); } @@ -979,9 +979,9 @@ Accounts.createUserVerifyingEmail = async (options) => { // that address. if (options.email && Accounts._options.sendVerificationEmail) { if (options.password) { - Accounts.sendVerificationEmail(userId, options.email); + await Accounts.sendVerificationEmail(userId, options.email); } else { - Accounts.sendEnrollmentEmail(userId, options.email); + await Accounts.sendEnrollmentEmail(userId, options.email); } } From 6799471779463c5ceb805b58a695f9f6387e1e17 Mon Sep 17 00:00:00 2001 From: filipenevola Date: Sat, 17 Dec 2022 08:11:49 -0400 Subject: [PATCH 5/7] Changes `Accounts.sendResetPasswordEmail`, `Accounts.sendEnrollmentEmail`,`Accounts.sendVerificationEmail` to async --- docs/history.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/history.md b/docs/history.md index 729b5b4e61..6ebff195e2 100644 --- a/docs/history.md +++ b/docs/history.md @@ -7,6 +7,11 @@ * `email`: `Email.send` is no longer available. Use `Email.sendAsync` instead. +* `accounts-password`: + - `Accounts.sendResetPasswordEmail` is now async + - `Accounts.sendEnrollmentEmail` is now async + - `Accounts.sendVerificationEmail` is now async + #### Internal API changes From 4868a2bb9411b9620e640696ed2d3c400b30b12c Mon Sep 17 00:00:00 2001 From: filipenevola Date: Sat, 17 Dec 2022 08:20:13 -0400 Subject: [PATCH 6/7] Changes `Accounts.sendLoginTokenEmail` to async --- docs/history.md | 5 ++++- .../accounts-passwordless/passwordless_server.js | 13 +++++++------ 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/docs/history.md b/docs/history.md index 6ebff195e2..e68b7c6f98 100644 --- a/docs/history.md +++ b/docs/history.md @@ -11,7 +11,10 @@ - `Accounts.sendResetPasswordEmail` is now async - `Accounts.sendEnrollmentEmail` is now async - `Accounts.sendVerificationEmail` is now async - + +* `accounts-passwordless`: + - `Accounts.sendLoginTokenEmail` is now async + #### Internal API changes diff --git a/packages/accounts-passwordless/passwordless_server.js b/packages/accounts-passwordless/passwordless_server.js index 382867926a..55f3f46222 100644 --- a/packages/accounts-passwordless/passwordless_server.js +++ b/packages/accounts-passwordless/passwordless_server.js @@ -110,7 +110,7 @@ function generateSequence() { } Meteor.methods({ - requestLoginTokenForUser: ({ selector, userData, options = {} }) => { + requestLoginTokenForUser: async ({ selector, userData, options = {} }) => { let user = Accounts._findUserByQuery(selector, { fields: { emails: 1 }, }); @@ -189,14 +189,15 @@ Meteor.methods({ : true; if (shouldSendLoginTokenEmail) { - tokens.forEach(({ email, sequence }) => { + const sendLogins = tokens.map(({ email, sequence }) => Accounts.sendLoginTokenEmail({ userId: user._id, sequence, email, ...(options.extra ? { extra: options.extra } : {}), - }); - }); + }) + ); + await Promise.all(sendLogins); } return result; @@ -213,7 +214,7 @@ Meteor.methods({ * @param {Object} options.extra Optional. Extra properties * @returns {Object} Object with {email, user, token, url, options} values. */ -Accounts.sendLoginTokenEmail = ({ userId, sequence, email, extra = {} }) => { +Accounts.sendLoginTokenEmail = async ({ userId, sequence, email, extra = {} }) => { const user = getUserById(userId); const url = Accounts.urls.loginToken(email, sequence); const options = Accounts.generateOptionsForEmail( @@ -223,7 +224,7 @@ Accounts.sendLoginTokenEmail = ({ userId, sequence, email, extra = {} }) => { 'sendLoginToken', { ...extra, sequence } ); - Email.send({ ...options, extra }); + await Email.sendAsync({ ...options, extra }); if (Meteor.isDevelopment) { console.log(`\nLogin Token url: ${url}`); } From 3800cb199882e7e9e25c5d0d769a82aa03183b7a Mon Sep 17 00:00:00 2001 From: filipenevola Date: Sat, 17 Dec 2022 08:24:58 -0400 Subject: [PATCH 7/7] Updates email.d.ts to include the new sendAsync --- packages/email/email.d.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/email/email.d.ts b/packages/email/email.d.ts index 71380d328e..a34ce0a17d 100644 --- a/packages/email/email.d.ts +++ b/packages/email/email.d.ts @@ -17,7 +17,9 @@ export namespace Email { packageSettings?: unknown; } + /** @deprecated */ function send(options: EmailOptions): void; + function sendAsync(options: EmailOptions): Promise; function hookSend(fn: (options: EmailOptions) => boolean): void; function customTransport(fn: (options: CustomEmailOptions) => void): void; }