diff --git a/graph/context.js b/graph/context.js index df4794302..d765fb8bd 100644 --- a/graph/context.js +++ b/graph/context.js @@ -7,6 +7,7 @@ const plugins = require('../services/plugins'); const { getBroker } = require('./subscriptions/broker'); const debug = require('debug')('talk:graph:context'); const { createLogger } = require('../services/logging'); +const { graphql } = require('graphql'); /** * Contains the array of plugins that provide context to the server, these top @@ -75,6 +76,39 @@ class Context { this.parent = parent; } + /** + * graphql will execute a graph request for the current context. + * + * @param {String} requestString A GraphQL language formatted string + * representing the requested operation. + * @param {Object} variableValues A mapping of variable name to runtime value + * to use for all variables defined in the requestString. + * @param {Object} rootValue The value provided as the first argument to + * resolver functions on the top level type (e.g. the query object type). + * @param {String} operationName The name of the operation to use if + * requestString contains multiple possible operations. Can be omitted if + * requestString contains only one operation. + * @returns {Promise} + */ + async graphql( + requestString, + variableValues = {}, + rootValue = {}, + operationName = undefined + ) { + // Perform the graph request directly using the graphql client. + return graphql( + // Use the connected graph schema. + this.connectors.graph.schema, + requestString, + rootValue, + // Use this, the context as the context. + this, + variableValues, + operationName + ); + } + /** * forSystem returns a system context object that can be used for internal * operations. @@ -92,7 +126,7 @@ class Context { // Attach the Context to the connectors. connectors.graph.Context = Context; -// Connect the connect based plugings after the server has started. +// Connect the connect based plugins after the server has started. plugins.defer('server', 'connect', ({ plugin, connect }) => { debug(`connecting plugin to connectors '${plugin.name}'`); diff --git a/services/mailer.js b/services/mailer.js index 60fbd3ef3..0ca88fa94 100644 --- a/services/mailer.js +++ b/services/mailer.js @@ -4,8 +4,9 @@ const kue = require('./kue'); const i18n = require('./i18n'); const path = require('path'); const fs = require('fs-extra'); -const { get, set, merge, template } = require('lodash'); +const { get, set, merge, template, isObject } = require('lodash'); const { TEMPLATE_LOCALS } = require('../middleware/staticTemplate'); +const Context = require('../graph/context'); const { SMTP_HOST, @@ -131,6 +132,14 @@ mailer.send = async options => { return; } + // Try to get the user id or the email address from the passed in options. If + // neither are provided, then error out. + const email = get(options, 'email'); + const user = isObject(options.user) ? get(options.user, 'id') : options.user; + if (!user && !email) { + throw new Error('user not provided'); + } + // Create the new locals object and attach the static locals and the i18n // framework. const locals = merge({}, mailer.helpers, options.locals, TEMPLATE_LOCALS, { @@ -147,9 +156,11 @@ mailer.send = async options => { // Create the job to send the email later. return mailer.task.create({ title: 'Mail', + user, + email, message: { - to: options.to, subject: `${EMAIL_SUBJECT_PREFIX} ${options.subject}`, + from: SMTP_FROM_ADDRESS, text, html, }, @@ -162,14 +173,48 @@ mailer.send = async options => { mailer.process = () => { debug(`Now processing ${mailer.task.name} jobs`); - return mailer.task.process(({ id, data }, done) => { + return mailer.task.process(async ({ id, data }, done) => { + const { email, user, message } = data; + debug(`Starting to send mail for Job[${id}]`); - // Set the `from` field. - data.message.from = SMTP_FROM_ADDRESS; + // If the message has a specific email already to sent it to, just assign + // that to the message. If the email does not have an email, and instead has + // a user id, then we should lookup the user with the graph and get their + // email. + if (email) { + message.to = email; + } else { + // Get the user to send the message to. + const ctx = Context.forSystem(); + + try { + const { data, errors } = await ctx.graphql( + ` + query GetUserEmail($user: ID!) { + user(id: $user) { + email + } + } + `, + { user } + ); + if (errors) { + return done(errors); + } + const email = get(data, 'user.email'); + if (!email) { + return done(errors.ErrMissingEmail); + } + + message.to = email; + } catch (err) { + return done(err); + } + } // Actually send the email. - mailer.transport.sendMail(data.message, err => { + mailer.transport.sendMail(message, err => { if (err) { debug(`Failed to send mail for Job[${id}]:`, err); return done(err); diff --git a/services/users.js b/services/users.js index 3b7d3b180..945f56fd5 100644 --- a/services/users.js +++ b/services/users.js @@ -400,8 +400,9 @@ class UsersService { /** * sendEmailConfirmation sends a confirmation email to the user. + * * @param {String} user the user to send the email to - * @param {String} email the email for the user to send the email to + * @param {String} email the email for the user to send the email to */ static async sendEmailConfirmation(user, email, redirectURI = ROOT_URL) { let token = await UsersService.createEmailConfirmToken( @@ -418,21 +419,14 @@ class UsersService { email, }, subject: i18n.t('email.confirm.subject'), - to: email, + email, }); } static async sendEmail(user, options) { - const email = user.firstEmail; - if (!email) { - // Rather than throwing an error here, we'll - console.warn(new Error('user does not have an email')); - return; - } - return mailer.send( merge({}, options, { - to: email, + user: user.id, }) ); }