Accounts enrollAccount/resetPassword/verifyEmail i18n

Hi, is there a way to provide an email template for the above mentioned actions, that depends on the user’s browser language?

When creating user save their language on their profile, then on server get that value from their profile and retrieve the proper language strings from your intl files.
That is the simple overview. Application will depend on which tech you use for i10n.

Great, thanks! I’ll try that.

Hi, so I tried to use my “re-written to be thread-safe” i18n mechanism which is inherently async because it uses dynamic imports for the i18n messages (it imports messages from modules). But “Accounts.emailTemplates.enrollAccount.text” does not work with async functions.

Accounts.emailTemplates.enrollAccount.text = async (user, url) => {
  const i18n = await new I18n(user.profile.languages, "en").load();
  const accountName = user.profile.displayName || user.emails[0].address;
  return i18n.get(["EmailTemplates", "enrollAccount", "text"], { accountName, url });
};

On Meteor 2.16 preparing for migration to 3.0, but looks like I hit a snag similar to the one described above:

the Accounts.emailTemplates.xxx.text and html do not work if the function needs to be async. I my case trying to retrieve a setting in a user related collection which now needs to use await. Something like:

    Accounts.emailTemplates.resetPassword.html = async (user, url) => {
       const org = await Orgs.findOneAsync({ orgCd: user.orgCd });
      ...
    }

Any ideas please?

If you are in a method, you can avail of this.userId and resolve anything async with it before you reach your Accounts.emailTemplates and disregard the user inside that function.

To understand how Accounts finds a relevant user for the function, you can start from here: meteor/packages/accounts-base/accounts_server.js at 45c6b54128d28f82ff9c23d038139f0f9d570131 · meteor/meteor · GitHub

You just need to resolve everything async outside.

I understand. I think :slight_smile:

However, how is that solving my issue, since the reason of that html(user, url) function is to return the content of the email, and the function only has those 2 parameters, so how can I generate a body of the email containing for example the name of the user Organisation? Pre-Meteor v3, the html function can retrieve the required data from the DB based on a user attribute and return that info included in the email content. How can the same be done in v3?

Ok, there are no many ways to say it better or different than how I said it already :slight_smile:

// resolve everything outside
const orgCd = (await Meteor.users.finOneAsync(this.userId, { fields: { orgCd: 1, _id: 0   }))?.orgCd
const org =  await Orgs.findOneAsync({ orgCd })

// run your function syncronous as before
Accounts.emailTemplates.resetPassword.html = async (user, url) => {
      // use org here
}

Hi @paulishca ,

Thanks for that. It works ok.

I’m just wondering, since Accounts is a global object, would not setting up the Accounts.emailTemplates.resetPassword.html function from each method invocation carry the risk of collisions between different users?

Ok, but wouldn’t that render Meteor totally useless?!
At the link I posted earlier, you can see how a user is being identified.

// ...
const currentInvocation = DDP._CurrentMethodInvocation.get() || DDP._CurrentPublicationInvocation.get();
return currentInvocation.userId

Accounts is partially global in the sense that the server side is global on the server and the client side is global on the client.
Do you mean to say that a user might use another user’s id and do an impersonation in order to obtain a password reset?!
You need to consider that a sessionId is being given to a user upon authentication. Once you are authenticated you cannot send another user ID to the server within that DDP connection with that sessionId

No, I did not mean that. See if I explain my concern (it may not be valid):

  1. Server method invoked by User 1, which belongs to org A. The method sets up the Accounts.emailTemplates.resetPassword.html function with details of Org A (say the html of the email includes the name of Org A).
  2. Same server method invoked by user 2, which belongs to Org B. The method sets up the same function, but with details of Org B being returned by the function.
  3. Method from User 1 invokes Accounts.sendResetPasswordEmail API. This happens just after 2. Would’t the html returned by the Accounts.emailTemplates.resetPassword.html function include the Org B name instead of the Org A name?

I’m assuming that the Accounts.sendResetPasswordEmail API is invoking the Accounts.emailTemplates.resetPassword.html function in the server Global Accounts object to generate the text of the email.

Thank you for your patience.

const funcA = () => {}
// and
const obj {
  funcA = () => {}
}

funcA() and obj.funcA() are both the exact same kind of function, they will run and return the same thing every time. I think you express the behavior of a global variable that may receive different values but these are functions not variables. Every call depends on a userId.

Accounts.emailTemplates.resetPassword.html is initialized with a function not with the result of a function.

Hi @paulishca,

So, I decided to test it out :wink:

Basic minimalistic code in a server method:

        const org = await Orgs.findOneAsync({ orgCd: u.orgCd })
        Accounts.emailTemplates.resetPassword = {
          subject() {
            return `How to reset your password`;
          },
          text(u, url) {
            const emailTemplate = 'Hey ${ u.givenName } from ${ org.name }! Verify your e-mail by following this link: ${ url }';
            const resetPasswordT = _.template(emailTemplate); // use lowdash template
            return resetPasswordT({ u, url, org });
          },
        };
        await Accounts.sendResetPasswordEmail(u._id, u.emails[0].address);

I have 2 users:

{ givenName: 'Paul', orgCd: 'SPO' },
{ givenName: 'Karl', orgCd: 'A' }

and 2 Orgs:

{ orgCd: 'A', name: 'Bupa' },
{ orgCd: 'SPO', name: 'Spotless' }

Invoking the method for user Paul generates this email text:

and the same for Karl:

So far so good. Now to simulate my concern of a race condition of the Global Accounts object that contains the function generating the email text, I added the following delay to user Paul before invoking the Accounts.sendResetPasswordEmail API:

        const org = await Orgs.findOneAsync({ orgCd: u.orgCd })
        Accounts.emailTemplates.resetPassword = {
          subject() {
            return `How to reset your password`;
          },
          text(u, url) {
            const emailTemplate = 'Hey ${ u.givenName } from ${ org.name }! Verify your e-mail by following this link: ${ url }';
            const resetPasswordT = _.template(emailTemplate); // use lowdash template
            return resetPasswordT({ u, url, org });
          },
        };
        if (u.givenName === 'Paul') {
          function sleep(ms) {
            return new Promise(resolve => setTimeout(resolve, ms));
          }
          console.log("sleep")
          await sleep(10000);
          console.log("wake")
        }
        await Accounts.sendResetPasswordEmail(u._id, u.emails[0].address);

Then simulate the race condition, by invoking the method for Paul and within 10 seconds invoking it for Karl. And this is the result of the email sent to Paul:

As you can see, Paul’s email includes Karl’s Org name.

This proves that your suggestion of resolving everything outside the text (or html) function before it is stored in the Accounts.emailTemplates.resetPassword object is not functionally equivalent to resolving everything inside the function like when in pre-3.0 sync like code:

        Accounts.emailTemplates.resetPassword = {
          subject() {
            return `How to reset your password`;
          },
          text(u, url) {
            const org = Orgs.findOne({ orgCd: u.orgCd });
            const emailTemplate = 'Hey ${ u.givenName } from ${ org.name }! Verify your e-mail by following this link: ${ url }';
            const resetPasswordT = _.template(emailTemplate); // use lowdash template
            return resetPasswordT({ u, url, org });
          },
        };
        await Accounts.sendResetPasswordEmail(u._id, u.emails[0].address);

We can probably say that it is not probable that this race condition happens because the setting up of the function code is followed immediately by the send email, but it is possible, and I would prefer not to take the risk to send emails themed incorrectly (different email colour theme, logos, etc, per organisation)

Also, note that before the creation of the function on the Accounts object was done once in initial configuration, and you suggestion requires it to be created in every method invocation.

I think this functionality should be maintained for Meteor 3, by allowing the Accounts.emailTemplates.resetPassword text / html functions to be defined async so that we can continue to resolve the email text based on code that needs to use async APIs.

Unless you can think of another solution?

Ok, do you mean to say that Paul is logged in a client and calls that method logged in as Paul and Karl is logged in another client calls the method logged in as Karl?

The method can be invoked either by an Admin from a User Management screen, or from a ‘forgot password’ link on the login screen by entering the username. In either case, the email is sent to the user verified email address. There are no client method stubs, just server side.

Right, but the user inside the Accounts is based on the DDP connection and … when someone else does it … you still call a method on the server, which is a server side async function, you don’t call the forgot password of 10 users within the same method (function).

I am not sure you can ever get in the race condition that you mentioned unless you do that inside 1 single method which is absolutely right.
So instead of calling 1 method for 10 users you would call 10 methods 1 for each user … kinda.

Yes, only one method invocation for each user requiring password resets. That example was that exactly. Each method invocation runs in the context of an Admin logged in user, or a no logged in user, passing in the username of a user that has a verified email address.

Nevertheless, requiring the email text function to be setup each time the method runs when that was clearly intended to be set up at server initialisation time in the global Accounts object, is not really a best approach. Also complicated by the fact that the email templates are kept in the private dir and retrieved via Assets.getText, which now also needs to be done once per method invocation because that is now also async (Assets.getTextAsync), and the password reset is not the only method that generates emails, as you have registration emails and email verification emails.

A lot of refactoring and inefficiencies if we can’t resolve the emails text from a predefined function assigned once to the Accounts email templates as intended. :frowning: