178 строки
6.2 KiB
JavaScript
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
|
|
};
|
|
};
|
|
};
|