1515using Microsoft . Extensions . Logging ;
1616using Microsoft . WindowsAzure . Storage ;
1717using Newtonsoft . Json ;
18+ using Newtonsoft . Json . Linq ;
1819using NuGet . Jobs ;
1920using NuGet . Services . KeyVault ;
2021using NuGet . Services . Sql ;
@@ -26,8 +27,7 @@ public class Job : JobBase
2627 {
2728 private readonly TimeSpan _defaultCommandTimeout = TimeSpan . FromMinutes ( 30 ) ;
2829
29- private readonly ConcurrentDictionary < string , DateTimeOffset > _contactedUsers = new ConcurrentDictionary < string , DateTimeOffset > ( ) ;
30- private readonly string _cursorFile = "cursor.json" ;
30+ private readonly string _cursorFile = "cursorv2.json" ;
3131
3232 private bool _whatIf = false ;
3333
@@ -39,7 +39,6 @@ public class Job : JobBase
3939 private string _mailFrom ;
4040 private SmtpClient _smtpClient ;
4141
42- private int _allowEmailResendAfterDays = 7 ;
4342 private int _warnDaysBeforeExpiration = 10 ;
4443
4544 private Storage _storage ;
@@ -64,9 +63,6 @@ public override void Init(IServiceContainer serviceContainer, IDictionary<string
6463 _warnDaysBeforeExpiration = JobConfigurationManager . TryGetIntArgument ( jobArgsDictionary , MyJobArgumentNames . WarnDaysBeforeExpiration )
6564 ?? _warnDaysBeforeExpiration ;
6665
67- _allowEmailResendAfterDays = JobConfigurationManager . TryGetIntArgument ( jobArgsDictionary , MyJobArgumentNames . AllowEmailResendAfterDays )
68- ?? _allowEmailResendAfterDays ;
69-
7066 var storageConnectionString = JobConfigurationManager . GetArgument ( jobArgsDictionary , JobArgumentNames . DataStorageAccount ) ;
7167 var storageContainerName = JobConfigurationManager . GetArgument ( jobArgsDictionary , JobArgumentNames . ContainerName ) ;
7268
@@ -77,89 +73,61 @@ public override void Init(IServiceContainer serviceContainer, IDictionary<string
7773
7874 public override async Task Run ( )
7975 {
76+ var jobRunTime = DateTimeOffset . UtcNow ;
77+ // Default values
78+ var jobCursor = new JobRunTimeCursor ( jobCursorTime : jobRunTime , maxProcessedCredentialsTime : jobRunTime ) ;
79+ var galleryCredentialExpiration = new GalleryCredentialExpiration ( new CredentialExpirationJobMetadata ( jobRunTime , _warnDaysBeforeExpiration , jobCursor ) , _galleryDatabase ) ;
80+
8081 try
8182 {
82- List < ExpiredCredentialData > expiredCredentials = null ;
83+ List < ExpiredCredentialData > credentialsInRange = null ;
8384
84- // Who did we contact before?
85+ // Get the most recent date for the emails being sent
8586 if ( _storage . Exists ( _cursorFile ) )
8687 {
8788 string content = await _storage . LoadString ( _storage . ResolveUri ( _cursorFile ) , CancellationToken . None ) ;
8889 // Load from cursor
89- var contactedUsers = JsonConvert . DeserializeObject < Dictionary < string , DateTimeOffset > > ( content ) ;
90-
91- // Clean older entries (contacted in last _allowEmailResendAfterDays)
92- var referenceDate = DateTimeOffset . UtcNow . AddDays ( - 1 * _allowEmailResendAfterDays ) ;
93- foreach ( var kvp in contactedUsers . Where ( kvp => kvp . Value >= referenceDate ) )
94- {
95- _contactedUsers . AddOrUpdate ( kvp . Key , kvp . Value , ( s , offset ) => kvp . Value ) ;
96- }
90+ // Throw if the schema is not correct to ensure that not-intended emails are sent.
91+ jobCursor = JsonConvert . DeserializeObject < JobRunTimeCursor > ( content , new JsonSerializerSettings ( ) { MissingMemberHandling = MissingMemberHandling . Error } ) ;
92+ galleryCredentialExpiration = new GalleryCredentialExpiration ( new CredentialExpirationJobMetadata ( jobRunTime , _warnDaysBeforeExpiration , jobCursor ) , _galleryDatabase ) ;
9793 }
9894
9995 // Connect to database
100- using ( var galleryConnection = await _galleryDatabase . CreateAsync ( ) )
101- {
102- // Fetch credentials that expire in _warnDaysBeforeExpiration days
103- // + the user's e-mail address
104- Logger . LogInformation ( "Retrieving expired credentials from Gallery database..." ) ;
105-
106- expiredCredentials = ( await galleryConnection . QueryWithRetryAsync < ExpiredCredentialData > (
107- Strings . GetExpiredCredentialsQuery ,
108- param : new { DaysBeforeExpiration = _warnDaysBeforeExpiration } ,
109- maxRetries : 3 ,
110- commandTimeout : _defaultCommandTimeout ) ) . ToList ( ) ;
111-
112- Logger . LogInformation ( "Retrieved {ExpiredCredentials} expired credentials." ,
113- expiredCredentials . Count ) ;
114- }
96+ Logger . LogInformation ( "Retrieving expired credentials from Gallery database..." ) ;
97+ credentialsInRange = await galleryCredentialExpiration . GetCredentialsAsync ( _defaultCommandTimeout ) ;
98+ Logger . LogInformation ( "Retrieved {ExpiredCredentials} expired credentials." ,
99+ credentialsInRange . Count ) ;
115100
116101 // Add default description for non-scoped API keys
117- expiredCredentials
102+ credentialsInRange
118103 . Where ( cred => string . IsNullOrEmpty ( cred . Description ) )
119104 . ToList ( )
120105 . ForEach ( ecd => ecd . Description = Constants . NonScopedApiKeyDescription ) ;
121106
122107 // Group credentials for each user
123- var userToExpiredCredsMapping = expiredCredentials
108+ var userToExpiredCredsMapping = credentialsInRange
124109 . GroupBy ( x => x . Username )
125110 . ToDictionary ( user => user . Key , value => value . ToList ( ) ) ;
126111
127- // Handle expiring credentials
128- var jobRunTime = DateTimeOffset . UtcNow ;
129112 foreach ( var userCredMapping in userToExpiredCredsMapping )
130113 {
131114 var username = userCredMapping . Key ;
132115 var credentialList = userCredMapping . Value ;
133116
134117 // Split credentials into two lists: Expired and Expiring to aggregate messages
135- var expiringCredentialList = credentialList
136- . Where ( x => ( x . Expires - jobRunTime ) . TotalDays > 0 )
137- . ToList ( ) ;
138- var expiredCredentialList = credentialList
139- . Where ( x => ( x . Expires - jobRunTime ) . TotalDays <= 0 )
140- . ToList ( ) ;
141-
142- DateTimeOffset userContactTime ;
143- if ( ! _contactedUsers . TryGetValue ( username , out userContactTime ) )
144- {
145- // send expiring API keys email notification
146- await HandleExpiredCredentialEmail ( username , expiringCredentialList , jobRunTime , expired : false ) ;
147-
148- // send expired API keys email notification
149- await HandleExpiredCredentialEmail ( username , expiredCredentialList , jobRunTime , expired : true ) ;
150- }
151- else
152- {
153- Logger . LogDebug ( "Skipping expired credential for user {Username} - already handled at {JobRuntime}." ,
154- username , userContactTime ) ;
155- }
118+ var expiringCredentialList = galleryCredentialExpiration . GetExpiringCredentials ( credentialList ) ;
119+ var expiredCredentialList = galleryCredentialExpiration . GetExpiredCredentials ( credentialList ) ;
120+
121+ await HandleExpiredCredentialEmail ( username , expiringCredentialList , jobRunTime , expired : false ) ;
122+
123+ // send expired API keys email notification
124+ await HandleExpiredCredentialEmail ( username , expiredCredentialList , jobRunTime , expired : true ) ;
156125 }
157126 }
158127 finally
159128 {
160- // Make sure we know who has been contacted today, so they do not get double
161- // e-mail notifications.
162- string json = JsonConvert . SerializeObject ( _contactedUsers ) ;
129+ JobRunTimeCursor newCursor = new JobRunTimeCursor ( jobCursorTime : jobRunTime , maxProcessedCredentialsTime : galleryCredentialExpiration . GetMaxNotificationDate ( ) ) ;
130+ string json = JsonConvert . SerializeObject ( newCursor ) ;
163131 var content = new StringStorageContent ( json , "application/json" ) ;
164132 await _storage . Save ( _storage . ResolveUri ( _cursorFile ) , content , CancellationToken . None ) ;
165133 }
@@ -172,9 +140,8 @@ private async Task HandleExpiredCredentialEmail(string username, List<ExpiredCre
172140 return ;
173141 }
174142
175- Logger . LogInformation ( "Handling {Expired} credential(s) for user {Username} (Keys: {Descriptions})..." ,
143+ Logger . LogInformation ( "Handling {Expired} credential(s) (Keys: {Descriptions})..." ,
176144 expired ? "expired" : "expiring" ,
177- username ,
178145 string . Join ( ", " , credentialList . Select ( x => x . Description ) . ToList ( ) ) ) ;
179146
180147 // Build message
@@ -206,21 +173,18 @@ private async Task HandleExpiredCredentialEmail(string username, List<ExpiredCre
206173 await _smtpClient . SendMailAsync ( mailMessage ) ;
207174 }
208175
209- Logger . LogInformation ( "Handled {Expired} credential for user {Username}." ,
210- expired ? "expired" : "expiring" ,
211- username ) ;
212-
213- _contactedUsers . AddOrUpdate ( username , jobRunTime , ( s , offset ) => jobRunTime ) ;
176+ Logger . LogInformation ( "Handled {Expired} credential ." ,
177+ expired ? "expired" : "expiring" ) ;
214178 }
215179 catch ( SmtpFailedRecipientException ex )
216180 {
217- var logMessage = "Failed to handle credential for user {Username} - recipient failed!" ;
218- Logger . LogWarning ( LogEvents . FailedToSendMail , ex , logMessage , username ) ;
181+ var logMessage = "Failed to handle credential - recipient failed!" ;
182+ Logger . LogWarning ( LogEvents . FailedToSendMail , ex , logMessage ) ;
219183 }
220184 catch ( Exception ex )
221185 {
222- var logMessage = "Failed to handle credential for user {Username} ." ;
223- Logger . LogCritical ( LogEvents . FailedToHandleExpiredCredential , ex , logMessage , username ) ;
186+ var logMessage = "Failed to handle credential ." ;
187+ Logger . LogCritical ( LogEvents . FailedToHandleExpiredCredential , ex , logMessage ) ;
224188
225189 throw ;
226190 }
0 commit comments