mirror of
https://github.com/meteor/meteor.git
synced 2026-05-02 03:01:46 -04:00
Merge pull request #12386 from meteor/release-3.0-email
Remove wrapAsync from email package
This commit is contained in:
@@ -1,3 +1,33 @@
|
||||
## v3.0, TBD
|
||||
|
||||
### Highlights
|
||||
|
||||
#### Breaking Changes
|
||||
|
||||
* `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
|
||||
|
||||
* `accounts-passwordless`:
|
||||
- `Accounts.sendLoginTokenEmail` is now async
|
||||
|
||||
#### 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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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}`);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
2
packages/email/email.d.ts
vendored
2
packages/email/email.d.ts
vendored
@@ -17,7 +17,9 @@ export namespace Email {
|
||||
packageSettings?: unknown;
|
||||
}
|
||||
|
||||
/** @deprecated */
|
||||
function send(options: EmailOptions): void;
|
||||
function sendAsync(options: EmailOptions): Promise<void>;
|
||||
function hookSend(fn: (options: EmailOptions) => boolean): void;
|
||||
function customTransport(fn: (options: CustomEmailOptions) => void): void;
|
||||
}
|
||||
|
||||
@@ -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,9 +250,28 @@ 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);
|
||||
};
|
||||
|
||||
/**
|
||||
* @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
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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: '<i>Cool</i>, 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: '<i>Cool</i>, 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: '<i>Cool</i>, 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',
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user