@@ -11,7 +11,7 @@ use crate::models::{
11
11
use crate :: util:: errors:: { AppResult , BoxedAppError , bad_request, crate_not_found, custom} ;
12
12
use crate :: views:: EncodableOwner ;
13
13
use crate :: { App , app:: AppState } ;
14
- use crate :: { auth:: AuthCheck , email:: Email } ;
14
+ use crate :: { auth:: AuthCheck , email:: EmailMessage } ;
15
15
use axum:: Json ;
16
16
use chrono:: Utc ;
17
17
use crates_io_github:: { GitHubClient , GitHubError } ;
@@ -20,9 +20,11 @@ use diesel_async::scoped_futures::ScopedFutureExt;
20
20
use diesel_async:: { AsyncConnection , AsyncPgConnection , RunQueryDsl } ;
21
21
use http:: StatusCode ;
22
22
use http:: request:: Parts ;
23
+ use minijinja:: context;
23
24
use oauth2:: AccessToken ;
24
- use secrecy:: { ExposeSecret , SecretString } ;
25
+ use secrecy:: ExposeSecret ;
25
26
use thiserror:: Error ;
27
+ use tracing:: warn;
26
28
27
29
#[ derive( Debug , Serialize , utoipa:: ToSchema ) ]
28
30
pub struct UsersResponse {
@@ -240,13 +242,20 @@ async fn modify_owners(
240
242
if let Some ( recipient) =
241
243
invitee. verified_email ( conn) . await . ok ( ) . flatten ( )
242
244
{
243
- emails. push ( OwnerInviteEmail {
244
- recipient_email_address : recipient,
245
- inviter : user. gh_login . clone ( ) ,
246
- domain : app. emails . domain . clone ( ) ,
247
- crate_name : krate. name . clone ( ) ,
248
- token,
249
- } ) ;
245
+ let email = EmailMessage :: from_template (
246
+ "owner_invite" ,
247
+ context ! {
248
+ inviter => user. gh_login,
249
+ domain => app. emails. domain,
250
+ crate_name => krate. name,
251
+ token => token. expose_secret( )
252
+ } ,
253
+ ) ;
254
+
255
+ match email {
256
+ Ok ( email_msg) => emails. push ( ( recipient, email_msg) ) ,
257
+ Err ( error) => warn ! ( "Failed to render owner invite email template: {error}" ) ,
258
+ }
250
259
}
251
260
}
252
261
@@ -291,11 +300,9 @@ async fn modify_owners(
291
300
292
301
// Send the accumulated invite emails now the database state has
293
302
// committed.
294
- for email in emails {
295
- let addr = email. recipient_email_address ( ) . to_string ( ) ;
296
-
297
- if let Err ( e) = app. emails . send ( & addr, email) . await {
298
- warn ! ( "Failed to send co-owner invite email: {e}" ) ;
303
+ for ( recipient, email) in emails {
304
+ if let Err ( error) = app. emails . send ( & recipient, email) . await {
305
+ warn ! ( "Failed to send owner invite email to {recipient}: {error}" ) ;
299
306
}
300
307
}
301
308
@@ -503,41 +510,3 @@ impl From<OwnerRemoveError> for BoxedAppError {
503
510
}
504
511
}
505
512
}
506
-
507
- pub struct OwnerInviteEmail {
508
- /// The destination email address for this email.
509
- recipient_email_address : String ,
510
-
511
- /// Email body variables.
512
- inviter : String ,
513
- domain : String ,
514
- crate_name : String ,
515
- token : SecretString ,
516
- }
517
-
518
- impl OwnerInviteEmail {
519
- pub fn recipient_email_address ( & self ) -> & str {
520
- & self . recipient_email_address
521
- }
522
- }
523
-
524
- impl Email for OwnerInviteEmail {
525
- fn subject ( & self ) -> String {
526
- format ! (
527
- "crates.io: Ownership invitation for \" {}\" " ,
528
- self . crate_name
529
- )
530
- }
531
-
532
- fn body ( & self ) -> String {
533
- format ! (
534
- "{user_name} has invited you to become an owner of the crate {crate_name}!\n
535
- Visit https://{domain}/accept-invite/{token} to accept this invitation,
536
- or go to https://{domain}/me/pending-invites to manage all of your crate ownership invitations." ,
537
- user_name = self . inviter,
538
- domain = self . domain,
539
- crate_name = self . crate_name,
540
- token = self . token. expose_secret( ) ,
541
- )
542
- }
543
- }
0 commit comments