44
55import { Container } from 'typedi' ;
66import { StatsD } from 'hot-shots' ;
7+ import isEqual from 'lodash/isEqual' ;
78
89import {
910 CloudTaskOptions ,
1011 EmailTypes ,
1112 SendEmailTaskPayload ,
1213 InactiveAccountEmailTasks ,
1314 InactiveAccountEmailTasksFactory ,
15+ ReasonForDeletion ,
16+ DeleteAccountTasks ,
1417} from '@fxa/shared/cloud-tasks' ;
1518import { ConnectedServicesDb } from 'fxa-shared/connected-services' ;
1619
@@ -27,6 +30,11 @@ import {
2730} from './active-status' ;
2831import { GleanMetricsType } from '../metrics/glean' ;
2932import { Logger } from 'mozlog' ;
33+ import {
34+ EmailBounce ,
35+ getAccountCustomerByUid ,
36+ } from 'fxa-shared/db/models/auth' ;
37+ import { BOUNCE_TYPES } from 'fxa-shared/db/models/auth/email-bounce' ;
3038
3139const aDayInMs = 24 * 60 * 60 * 1000 ;
3240const sixtyDaysInMs = 60 * aDayInMs ;
@@ -130,6 +138,7 @@ export class InactiveAccountsManager {
130138 fxaDb : DB ;
131139 oauthDb : ConnectedServicesDb ;
132140 accountEventsManager : AccountEventsManager ;
141+ accountTasks : DeleteAccountTasks ;
133142 mailer : any ;
134143 emailCloudTasks : InactiveAccountEmailTasks ;
135144 statsd : StatsD ;
@@ -156,6 +165,7 @@ export class InactiveAccountsManager {
156165 this . fxaDb = fxaDb ;
157166 this . oauthDb = oauthDb ;
158167 this . accountEventsManager = Container . get ( AccountEventsManager ) ;
168+ this . accountTasks = Container . get ( DeleteAccountTasks ) ;
159169 this . mailer = mailer ;
160170 this . emailCloudTasks = InactiveAccountEmailTasksFactory ( config , statsd ) ;
161171 this . statsd = statsd ;
@@ -216,9 +226,21 @@ export class InactiveAccountsManager {
216226 return ;
217227 }
218228
229+ const account = await this . fxaDb . account ( taskPayload . uid ) ;
230+ const now = Date . now ( ) ;
231+
232+ // If the very first email hard bounced, we'll delete the account without
233+ // sending further emails.
234+ if (
235+ taskPayload . emailType ===
236+ EmailTypes . INACTIVE_DELETE_SECOND_NOTIFICATION &&
237+ ( await this . handleFirstEmailBounce ( taskPayload , account , now ) )
238+ ) {
239+ return ;
240+ }
241+
219242 // Check to see if we have sent this email before in the current
220243 // notify-and-delete cycle
221- const now = Date . now ( ) ;
222244 const sentEmailEvents = await this . accountEventsManager . findEmailEvents (
223245 taskPayload . uid ,
224246 'emailSent' ,
@@ -251,7 +273,6 @@ export class InactiveAccountsManager {
251273 return ;
252274 }
253275
254- const account = await this . fxaDb . account ( taskPayload . uid ) ;
255276 const message = {
256277 acceptLanguage : account . locale ,
257278 inactiveDeletionEta : now + emailTypeSpecificVals . timeToDeletion ,
@@ -331,4 +352,100 @@ export class InactiveAccountsManager {
331352 }
332353 }
333354 }
355+
356+ async handleFirstEmailBounce ( taskPayload , account , now ) {
357+ const emails = account . emails . map ( ( e ) => e . email ) ;
358+
359+ // Because a small number of first notification emails were sent before
360+ // the email types were added to the database
361+ // (https://github.com/mozilla/fxa/pull/18362), we need to carve out an
362+ // exception for a faction of those accounts. This can be removed after
363+ // 2025-04-12.
364+ // Delete the corresponding test when deleting this if block.
365+ if (
366+ account . createdAt >=
367+ new Date ( '2023-01-01' ) . setUTCHours ( 0 , 0 , 0 , 0 ) . valueOf ( ) &&
368+ account . createdAt <
369+ new Date ( '2023-02-01' ) . setUTCHours ( 0 , 0 , 0 , 0 ) . valueOf ( )
370+ ) {
371+ // check to see if there's a bounced email in Firestore and there's a hard bounce in MySQL with no email type
372+ const bounceEvents = await this . accountEventsManager . findEmailEvents (
373+ taskPayload . uid ,
374+ 'emailBounced' ,
375+ 'inactiveAccountFirstWarning' ,
376+ // some wiggle room to account for task and email delays
377+ now - 54 * aDayInMs ,
378+ now - 52 * aDayInMs
379+ ) ;
380+
381+ if ( bounceEvents . length > 0 ) {
382+ const bounceTime = bounceEvents . map ( ( x ) => x . createdAt ) ;
383+ const min = Math . min ( ...bounceTime ) ;
384+ const max = Math . max ( ...bounceTime ) ;
385+ const hardBounces = await EmailBounce . query ( )
386+ . whereIn ( 'email' , emails )
387+ . whereNull ( 'emailTypeId' )
388+ . where ( 'bounceType' , '=' , BOUNCE_TYPES . Permanent )
389+ . where ( 'createdAt' , '>' , min - 15000 )
390+ . where ( 'createdAt' , '<' , max + 15000 )
391+ . select ( [ 'email' , 'createdAt' ] ) ;
392+
393+ if ( bounceEvents . length === hardBounces . length ) {
394+ await this . accountTasks . deleteAccount ( {
395+ uid : taskPayload . uid ,
396+ customerId : (
397+ await getAccountCustomerByUid ( taskPayload . uid )
398+ ) ?. stripeCustomerId ,
399+ reason : ReasonForDeletion . InactiveAccountEmailBounced ,
400+ } ) ;
401+ this . statsd . increment ( `account.inactive.second-email.skipped.bounce` ) ;
402+ await this . glean . inactiveAccountDeletion . secondEmailSkipped (
403+ requestForGlean ,
404+ {
405+ uid : taskPayload . uid ,
406+ reason : 'first_email_bounced' ,
407+ }
408+ ) ;
409+ return true ;
410+ }
411+ }
412+ }
413+
414+ const firstEmailBounces = await EmailBounce . query ( )
415+ . join ( 'emailTypes' , 'emailTypes.id' , 'emailBounces.emailTypeId' )
416+ . whereIn ( 'emailBounces.email' , emails )
417+ . where ( 'emailBounces.bounceType' , '=' , BOUNCE_TYPES . Permanent )
418+ // some wiggle room to account for cloud task and email delays
419+ . where ( 'emailBounces.createdAt' , '>' , now - 54 * aDayInMs )
420+ . where ( 'emailBounces.createdAt' , '<' , now - 52 * aDayInMs )
421+ . where ( 'emailTypes.emailType' , '=' , 'inactiveAccountFirstWarning' )
422+ . select ( [ 'email' ] ) ;
423+
424+ if (
425+ firstEmailBounces . length === emails . length &&
426+ isEqual (
427+ firstEmailBounces . map ( ( x ) => x . email ) ,
428+ emails
429+ )
430+ ) {
431+ await this . accountTasks . deleteAccount ( {
432+ uid : taskPayload . uid ,
433+ customerId : (
434+ await getAccountCustomerByUid ( taskPayload . uid )
435+ ) ?. stripeCustomerId ,
436+ reason : ReasonForDeletion . InactiveAccountEmailBounced ,
437+ } ) ;
438+ this . statsd . increment ( `account.inactive.second-email.skipped.bounce` ) ;
439+ await this . glean . inactiveAccountDeletion . secondEmailSkipped (
440+ requestForGlean ,
441+ {
442+ uid : taskPayload . uid ,
443+ reason : 'first_email_bounced' ,
444+ }
445+ ) ;
446+ return true ;
447+ }
448+
449+ return false ;
450+ }
334451}
0 commit comments