@@ -35,8 +35,9 @@ public class Job : JobBase
3535 private string _mailFrom ;
3636 private SmtpClient _smtpClient ;
3737
38+ private int _allowEmailResendAfterDays = 7 ;
3839 private int _warnDaysBeforeExpiration = 10 ;
39-
40+
4041 private ILogger _logger ;
4142
4243 public override bool Init ( IDictionary < string , string > jobArgsDictionary )
@@ -64,15 +65,15 @@ public override bool Init(IDictionary<string, string> jobArgsDictionary)
6465 var smtpUri = new SmtpUri ( new Uri ( smtpConnectionString ) ) ;
6566 _smtpClient = CreateSmtpClient ( smtpUri ) ;
6667
67- var temp = JobConfigurationManager . TryGetIntArgument ( jobArgsDictionary , MyJobArgumentNames . WarnDaysBeforeExpiration ) ;
68- if ( temp . HasValue )
69- {
70- _warnDaysBeforeExpiration = temp . Value ;
71- }
68+ _warnDaysBeforeExpiration = JobConfigurationManager . TryGetIntArgument ( jobArgsDictionary , MyJobArgumentNames . WarnDaysBeforeExpiration )
69+ ?? _warnDaysBeforeExpiration ;
70+
71+ _allowEmailResendAfterDays = JobConfigurationManager . TryGetIntArgument ( jobArgsDictionary , MyJobArgumentNames . AllowEmailResendAfterDays )
72+ ?? _allowEmailResendAfterDays ;
7273 }
7374 catch ( Exception exception )
7475 {
75- _logger . LogCritical ( "Failed to initialize job! {Exception}" , exception ) ;
76+ _logger . LogCritical ( LogEvents . JobInitFailed , exception , "Failed to initialize job!" ) ;
7677
7778 return false ;
7879 }
@@ -93,8 +94,8 @@ public override async Task<bool> Run()
9394 var contactedUsers = JsonConvert . DeserializeObject < Dictionary < string , DateTimeOffset > > (
9495 File . ReadAllText ( _cursorFile ) ) ;
9596
96- // Clean older entries (contacted in last _warnDaysBeforeExpiration * 2 days )
97- var referenceDate = DateTimeOffset . UtcNow . AddDays ( - 2 * _warnDaysBeforeExpiration ) ;
97+ // Clean older entries (contacted in last _allowEmailResendAfterDays )
98+ var referenceDate = DateTimeOffset . UtcNow . AddDays ( - 1 * _allowEmailResendAfterDays ) ;
9899 foreach ( var kvp in contactedUsers . Where ( kvp => kvp . Value >= referenceDate ) )
99100 {
100101 _contactedUsers . AddOrUpdate ( kvp . Key , kvp . Value , ( s , offset ) => kvp . Value ) ;
@@ -118,24 +119,51 @@ public override async Task<bool> Run()
118119 expiredCredentials . Count ) ;
119120 }
120121
122+ // Add default description for non-scoped API keys
123+ expiredCredentials
124+ . Where ( cred => string . IsNullOrEmpty ( cred . Description ) )
125+ . ToList ( )
126+ . ForEach ( ecd => ecd . Description = Constants . NonScopedApiKeyDescription ) ;
127+
128+ // Group credentials for each user
129+ var userToExpiredCredsMapping = expiredCredentials
130+ . GroupBy ( x => x . Username )
131+ . ToDictionary ( user => user . Key , value => value . ToList ( ) ) ;
132+
121133 // Handle expiring credentials
122134 var jobRunTime = DateTimeOffset . UtcNow ;
123- foreach ( var expiredCredential in expiredCredentials )
135+ foreach ( var userCredMapping in userToExpiredCredsMapping )
124136 {
125- if ( ! _contactedUsers . ContainsKey ( expiredCredential . Username ) )
137+ var username = userCredMapping . Key ;
138+ var credentialList = userCredMapping . Value ;
139+
140+ // Split credentials into two lists: Expired and Expiring to aggregate messages
141+ var expiringCredentialList = credentialList
142+ . Where ( x => ( x . Expires - jobRunTime ) . TotalDays > 0 )
143+ . ToList ( ) ;
144+ var expiredCredentialList = credentialList
145+ . Where ( x => ( x . Expires - jobRunTime ) . TotalDays <= 0 )
146+ . ToList ( ) ;
147+
148+ DateTimeOffset userContactTime ;
149+ if ( ! _contactedUsers . TryGetValue ( username , out userContactTime ) )
126150 {
127- await HandleExpiredCredentialEmail ( expiredCredential , jobRunTime ) ;
151+ // send expiring API keys email notification
152+ await HandleExpiredCredentialEmail ( username , expiringCredentialList , jobRunTime , expired : false ) ;
153+
154+ // send expired API keys email notification
155+ await HandleExpiredCredentialEmail ( username , expiredCredentialList , jobRunTime , expired : true ) ;
128156 }
129157 else
130158 {
131- _logger . LogDebug ( "Skipping expired credential for user {Username} - already handled today ." ,
132- expiredCredential . Username ) ;
159+ _logger . LogDebug ( "Skipping expired credential for user {Username} - already handled at {JobRuntime} ." ,
160+ username , userContactTime ) ;
133161 }
134162 }
135163 }
136164 catch ( Exception ex )
137165 {
138- _logger . LogCritical ( "Job run failed! {Exception}" , ex ) ;
166+ _logger . LogCritical ( LogEvents . JobRunFailed , ex , "Job run failed!" ) ;
139167
140168 return false ;
141169 }
@@ -149,24 +177,37 @@ public override async Task<bool> Run()
149177 return true ;
150178 }
151179
152- private async Task HandleExpiredCredentialEmail ( ExpiredCredentialData expiredCredential , DateTimeOffset jobRunTime )
180+ private async Task HandleExpiredCredentialEmail ( string username , List < ExpiredCredentialData > credentialList , DateTimeOffset jobRunTime , bool expired )
153181 {
154- _logger . LogInformation ( "Handling expired credential for user {Username} (expires: {Expires})..." , expiredCredential . Username , expiredCredential . Expires ) ;
182+ if ( credentialList == null || credentialList . Count == 0 )
183+ {
184+ return ;
185+ }
186+
187+ _logger . LogInformation ( "Handling {Expired} credential(s) for user {Username} (Keys: {Descriptions})..." ,
188+ expired ? "expired" : "expiring" ,
189+ username ,
190+ string . Join ( ", " , credentialList . Select ( x => x . Description ) . ToList ( ) ) ) ;
155191
156192 // Build message
157- var mailMessage = new MailMessage ( _mailFrom , expiredCredential . EmailAddress ) ;
193+ var userEmail = credentialList . FirstOrDefault ( ) . EmailAddress ;
194+ var mailMessage = new MailMessage ( _mailFrom , userEmail ) ;
158195
196+ var apiKeyExpiryMessageList = credentialList
197+ . Select ( x => BuildApiKeyExpiryMessage ( x . Description , x . Expires , jobRunTime ) )
198+ . ToList ( ) ;
199+
200+ var apiKeyExpiryMessage = string . Join ( Environment . NewLine , apiKeyExpiryMessageList ) ;
159201 // Build email body
160- var expiresInDays = expiredCredential . Expires . UtcDateTime - DateTime . UtcNow ;
161- if ( expiresInDays . TotalDays <= 0 )
202+ if ( expired )
162203 {
163204 mailMessage . Subject = string . Format ( Strings . ExpiredEmailSubject , _galleryBrand ) ;
164- mailMessage . Body = string . Format ( Strings . ExpiredEmailBody , _galleryBrand , _galleryAccountUrl ) ;
205+ mailMessage . Body = string . Format ( Strings . ExpiredEmailBody , username , _galleryBrand , apiKeyExpiryMessage , _galleryAccountUrl ) ;
165206 }
166207 else
167208 {
168209 mailMessage . Subject = string . Format ( Strings . ExpiringEmailSubject , _galleryBrand ) ;
169- mailMessage . Body = string . Format ( Strings . ExpiringEmailBody , _galleryBrand , _galleryAccountUrl , ( int ) expiresInDays . TotalDays ) ;
210+ mailMessage . Body = string . Format ( Strings . ExpiringEmailBody , username , _galleryBrand , apiKeyExpiryMessage , _galleryAccountUrl ) ;
170211 }
171212
172213 // Send email
@@ -177,22 +218,37 @@ private async Task HandleExpiredCredentialEmail(ExpiredCredentialData expiredCre
177218 await _smtpClient . SendMailAsync ( mailMessage ) ;
178219 }
179220
180- _logger . LogInformation ( "Handled expired credential for user {Username}." , expiredCredential . Username ) ;
221+ _logger . LogInformation ( "Handled {Expired} credential for user {Username}." ,
222+ expired ? "expired" : "expiring" ,
223+ username ) ;
181224
182- _contactedUsers . AddOrUpdate ( expiredCredential . Username , jobRunTime , ( s , offset ) => jobRunTime ) ;
225+ _contactedUsers . AddOrUpdate ( username , jobRunTime , ( s , offset ) => jobRunTime ) ;
183226 }
184227 catch ( SmtpFailedRecipientException ex )
185228 {
186- _logger . LogWarning ( "Failed to handle expired credential for user {Username} - recipient failed." , expiredCredential . Username , ex ) ;
229+ var logMessage = $ "Failed to handle credential for user { username } - recipient failed!";
230+ _logger . LogWarning ( LogEvents . FailedToSendMail , ex , logMessage ) ;
187231 }
188232 catch ( Exception ex )
189233 {
190- _logger . LogCritical ( "Failed to handle expired credential for user {Username}." , expiredCredential . Username , ex ) ;
234+ var logMessage = $ "Failed to handle credential for user { username } .";
235+ _logger . LogCritical ( LogEvents . FailedToHandleExpiredCredential , ex , logMessage ) ;
191236
192237 throw ;
193238 }
194239 }
195240
241+ private static string BuildApiKeyExpiryMessage ( string description , DateTimeOffset expiry , DateTimeOffset currentTime )
242+ {
243+ var expiryInDays = ( expiry - currentTime ) . TotalDays ;
244+ var message = expiryInDays < 0
245+ ? string . Format ( Strings . ApiKeyExpired , description )
246+ : string . Format ( Strings . ApiKeyExpiring , description , ( int ) expiryInDays ) ;
247+
248+ // \u2022 - Unicode for bullet point.
249+ return "\u2022 " + message + Environment . NewLine ;
250+ }
251+
196252 private SmtpClient CreateSmtpClient ( SmtpUri smtpUri )
197253 {
198254 var smtpClient = new SmtpClient ( smtpUri . Host , smtpUri . Port )
0 commit comments