fxa-auth-server/lib/email/bounces.js

178 строки
6.2 KiB
JavaScript

/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
'use strict';
const eaddrs = require('email-addresses');
const P = require('./../promise');
const utils = require('./utils/helpers');
const isValidEmailAddress = require('./../routes/validators').isValidEmailAddress;
const SIX_HOURS = 1000 * 60 * 60 * 6;
module.exports = function (log, error) {
return function start(bounceQueue, db) {
function accountDeleted(uid, email) {
log.info('accountDeleted', { uid: uid, email: email });
}
function gotError(email, err) {
log.error('databaseError', { email: email, err: err });
}
function findEmailRecord(email) {
return db.accountRecord(email);
}
function recordBounce(bounce) {
return db.createEmailBounce(bounce);
}
function deleteAccountIfUnverifiedNew(record) {
// if account is not verified and younger than 6 hours then delete it.
if (! record.emailVerified && record.createdAt && record.createdAt > Date.now() - SIX_HOURS) {
return db.deleteAccount(record)
.then(
accountDeleted.bind(null, record.uid, record.email),
gotError.bind(null, record.email)
);
}
}
function handleBounce(message) {
utils.logErrorIfHeadersAreWeirdOrMissing(log, message, 'bounce');
let recipients = [];
// According to the AWS SES docs, a notification will never
// include multiple types, so it's fine for us to check for
// EITHER bounce OR complaint here.
if (message.bounce) {
recipients = message.bounce.bouncedRecipients;
} else if (message.complaint) {
recipients = message.complaint.complainedRecipients;
}
// SES can now send custom headers if enabled on topic.
// Headers are stored as an array of name/value pairs.
// Log the `X-Template-Name` header to help track the email template that bounced.
// Ref: http://docs.aws.amazon.com/ses/latest/DeveloperGuide/notification-contents.html
const templateName = utils.getHeaderValue('X-Template-Name', message);
const language = utils.getHeaderValue('Content-Language', message);
return P.each(recipients, (recipient) => {
// The email address in the bounce message has been handled by an external
// system, and depending on the system it can have had some strange things
// done to it. Try to normalize as best we can.
let email;
let emailIsValid = true;
const parsedAddress = eaddrs.parseOneAddress(recipient.emailAddress);
if (parsedAddress !== null) {
email = parsedAddress.address;
} else {
email = recipient.emailAddress;
if (! isValidEmailAddress(email)) {
emailIsValid = false;
// We couldn't make the recipient address look like a valid email.
// Log a warning but don't error out because we still want to
// emit flow metrics etc.
log.warn('handleBounce.addressParseFailure', {
email: email,
action: recipient.action,
diagnosticCode: recipient.diagnosticCode
});
}
}
const emailDomain = utils.getAnonymizedEmailDomain(email);
const logData = {
action: recipient.action,
email: email,
domain: emailDomain,
bounce: !! message.bounce,
diagnosticCode: recipient.diagnosticCode,
status: recipient.status
};
const bounce = {
email: email
};
// Template name corresponds directly with the email template that was used
if (templateName) {
logData.template = templateName;
}
if (language) {
logData.lang = language;
}
// Log the type of bounce that occurred
// Ref: http://docs.aws.amazon.com/ses/latest/DeveloperGuide/notification-contents.html#bounce-types
if (message.bounce && message.bounce.bounceType) {
bounce.bounceType = logData.bounceType = message.bounce.bounceType;
if (message.bounce.bounceSubType) {
bounce.bounceSubType = logData.bounceSubType = message.bounce.bounceSubType;
}
} else if (message.complaint) {
// Log the type of complaint and userAgent reported
logData.complaint = !! message.complaint;
bounce.bounceType = 'Complaint';
if (message.complaint.userAgent) {
logData.complaintUserAgent = message.complaint.userAgent;
}
if (message.complaint.complaintFeedbackType) {
bounce.bounceSubType = logData.complaintFeedbackType = message.complaint.complaintFeedbackType;
}
}
// Log the bounced flowEvent and emailEvent metrics
utils.logFlowEventFromMessage(log, message, 'bounced');
utils.logEmailEventFromMessage(log, message, 'bounced', emailDomain);
log.info('handleBounce', logData);
/**
* Docs: https://docs.aws.amazon.com/ses/latest/DeveloperGuide/notification-contents.html#bounce-types
* Bug: https://github.com/mozilla/fxa-content-server/issues/5629
*
* If there is any type of bounce then suggest account for deletion.
* Code below will fetch the email record and if it is an unverified new account then it will delete
* the account.
*/
const suggestAccountDeletion = !! bounce.bounceType;
const work = [];
if (emailIsValid) {
work.push(recordBounce(bounce)
.catch(gotError.bind(null, email)));
if (suggestAccountDeletion) {
work.push(findEmailRecord(email)
.then(
deleteAccountIfUnverifiedNew,
gotError.bind(null, email)
));
}
}
return P.all(work);
}).then(
() => {
// We always delete the message, even if handling some addrs failed.
message.del();
}
);
}
bounceQueue.on('data', handleBounce);
bounceQueue.start();
return {
bounceQueue: bounceQueue,
handleBounce: handleBounce
};
};
};