Customizing individual enrollment emails?


#1

I am trying to extend the content of meteor’s enrollment system. In particular, my invitation system is a little more “fine-grained”, in that users are invited to individual groups within the site.

The docs say you can set up the templates like so.

Accounts.emailTemplates.siteName = "AwesomeSite";
Accounts.emailTemplates.from = "AwesomeSite Admin <accounts@example.com>";
Accounts.emailTemplates.enrollAccount.subject = function (user) {
    return "Welcome to Awesome Town, " + user.profile.name;
};

However, can this be set up to be more variable rather than just being initialized on startup? I am trying to make a more dynamic invitation system. Eg:

Meteor.methods('Player.inviteToGuild', function (playerDoc, guildDoc) {
  var newUser = Accounts.createUser({
    email: playerDoc.email,
    profile: playerDoc
  });
  // change around the email templates here
  Accounts.sendEnrollmentEmail(newUser, { 
    subject: "You have been invited to join " + guildDoc.name + "!"
  });
});

Would this be possible?


#2

Have a look there to see how the sendEnrollmentEmail is implemented. You can generate enrollment url and send it with your custom email.

I think this code is essential to generate the url:

var token = Random.secret();
var when = new Date();
var tokenRecord = {
  token: token,
  email: email,
  when: when
};
Meteor.users.update(userId, {$set: {
  "services.password.reset": tokenRecord
}});

// before passing to template, update user object with new token
Meteor._ensure(user, 'services', 'password').reset = tokenRecord;

var enrollAccountUrl = Accounts.urls.enrollAccount(token);

There seems to be no straightforward solution to this.


#3

FWIW - We just bypassed the default mailer entirely (by over-riding the default functions) and use Mandrill templates to send everything. So, we have a set of templates on Mandrill with various merge tags in them. Then we just call the appropriate template with the corresponding merge data to customize our emails.


#4

@skwasha Do you have any example code on how you implemented the email bypass with Mandrill? This is exactly what we want to do as well. Thanks!


#5

@skirunman Here’s an example of how we send a Forgot Password email. Keep in mind we’ve actually got things all packaged into internal packages for accounts, mailer, etc. So, I’m extrapolating a bit, but it’s pretty basic. Just override the base account functionality where you need to do something different.

/server/accounts.coffee:

mailOpts = {}
# This is lifted from accounts-password (@1.1.1) so we can customize the email sent to use our Mandrill templates
# 
# send the user an email with a link that when opened allows the user
# to set a new password, without the old password.

# @summary Send an email with a link the user can use to reset their password.
# @locus Server
# @param {String} userId The id of the user to send email to.
# @param {String} [email] Optional. Which address of the user's to send the email to. This address must be in the user's `emails` list. Defaults to the first email in the list.
# 
Accounts.sendResetPasswordEmail = (userId, email)->
  # Make sure the user exists, and email is one of their addresses.
  user = Meteor.users.findOne userId
  if !user
    throw new Error("Can't find user")
  # pick the first email if we weren't passed an email.
  if !email && user.emails && user.emails[0]
    email = user.emails[0].address
  # make sure we have a valid email
  if !email or !_.contains(_.pluck(user.emails or [], 'address'), email)
    throw new Error('No such email for user.')

  token = Random.secret()
  whn = new Date()
  tokenRecord =
    token: token
    email: email
    when: whn

  Meteor.users.update userId, $set: "services.password.reset": tokenRecord
  
  # before passing to template, update user object with new token
  Meteor._ensure(user, 'services', 'password').reset = tokenRecord

  mailOpts.token = Meteor.absoluteUrl('reset/' + token)
  # This sends the forgot-password template using our mailer
  OUR.mailer.send "forgot-password", userId, null, email, mailOpts
  return

Then in your mailer:

##################################################################
# Methods for
# Mailer via Mandrill
##################################################################
_sendTemplate = (template, options, vars = [])->
 OUR.utils.log.info "Sending mail template", template, "to", options.email

  vars = for opt, val of options
    name: opt
    content: val
  Meteor.Mandrill.sendTemplate
    template_name: template
    template_content: [
      {}
    ]
    message:
      global_merge_vars: [
        name: "var1"
        content: "Global Value 1"
      ]
      merge_vars: [
        rcpt: options.email
        vars: vars
      ]
      to: [
        email: options.email
      ]
      bcc_address: options.bcc_address

@OUR.utils.namespace.addTo "mailer",
  send: (template, userId, ticket, email, options={})->
    switch template
      when 'forgot-password'
        user = Meteor.users.findOne(userId)
        return unless user
        return unless user.emails[0].address == email

        options.email = user.emails[0].address
        options.fname = options.email
        options.fname = user.profile.firstName if user.profile?.firstName?

    _sendTemplate(template, options)

Meteor.startup ->
  Meteor.Mandrill.config {
    username: process.env.MANDRILL_USER,
    key: process.env.MANDRILL_KEY
  }
  
  Meteor.methods
    mailCurrentUser: (template, options={})->
      OUR.mailer.send(template, @userId, null, null, options)

We have cases for all the various transactional mails we might send. New signup, forgot password, service related stuff, etc.


#6

There’s a much simpler way to customize Meteor’s outgoing emails with Mandrill templates, that does not involve rewriting Meteor’s Accounts email functions. It uses an often overlooked API call from Mandrill: templates.render. I’ve updated wylio:mandrill’s README to explain how to do this.

The idea is to set Accounts.emailTemplates.enrollAccount.html to the result of the render call, passing the template name and parameters:

Accounts.emailTemplates.enrollAccount.html = function (user, url) {
  console.log('Enrolling', user);
  var referralCode = ... // generate a secret
  var result;
  try {
    result = Mandrill.templates.render({
      template_name: 'sendwithus-skyline-welcome',
      template_content: [
        {
          name: 'referrallink',
          content: 'https://iDoRecall.com/?sref=' + referralCode
        }
      ]
    });
  } catch (error) {
    console.error('Error while rendering Mandrill template', error);
  }
  return result.html;
}

Then you let Meteor do its email sending magic by configuring MAIL_URL to point to your Mandrill account:

export MAIL_URL=smtp://you@yourdomain.com:password@smtp.mandrillapp.com:465

There are two problems with this approach:

  1. You can’t easily generate the text part of the message. Mandrill has an option for that, but Meteor doesn’t cooperate. The workaround is to put a good text default in the code for the very small percentage of users who can’t read HTML emails.

  2. For some reason, the render call doesn’t support Handlebars. This is quite odd, since internally they are probably calling render before sending out the email. The workaround is to use the old-school *|NAME|* merge vars.

Anyway, with a manually-written text message and old-style merge vars, we use this in production and you can try it live at https://iDoRecall.com.


#7

Thanks ! I have used it successfully on my application.

The only thing I left of was the Meteor._ensure line. It gave me an error saying “TypeError: Cannot use ‘in’ operator to search for ‘services’ in EyZT6cE7Zc9itzGxs”.

But the sparce information I found on it made me think it is being used just as a reassurance of some sort. It worked after taking this line out.