Skip to content

Commit 68064d3

Browse files
authored
controllers/krate/delete: Send deletion notification email after successful deletion (#10265)
1 parent ef31905 commit 68064d3

4 files changed

+151
-0
lines changed

src/controllers/krate/delete.rs

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
use crate::app::AppState;
22
use crate::auth::AuthCheck;
33
use crate::controllers::krate::CratePath;
4+
use crate::email::Email;
45
use crate::models::{NewDeletedCrate, Rights};
56
use crate::schema::{crate_downloads, crates, dependencies};
67
use crate::util::errors::{custom, AppResult, BoxedAppError};
@@ -80,6 +81,7 @@ pub async fn delete_crate(path: CratePath, parts: Parts, app: AppState) -> AppRe
8081
}
8182
}
8283

84+
let crate_name = krate.name.clone();
8385
conn.transaction(|conn| {
8486
async move {
8587
diesel::delete(crates::table.find(krate.id))
@@ -117,6 +119,23 @@ pub async fn delete_crate(path: CratePath, parts: Parts, app: AppState) -> AppRe
117119
})
118120
.await?;
119121

122+
let email_future = async {
123+
if let Some(recipient) = user.email(&mut conn).await? {
124+
let email = CrateDeletionEmail {
125+
user: &user.gh_login,
126+
krate: &crate_name,
127+
};
128+
129+
app.emails.send(&recipient, email).await?
130+
}
131+
132+
Ok::<_, anyhow::Error>(())
133+
};
134+
135+
if let Err(err) = email_future.await {
136+
error!("Failed to send crate deletion email: {err}");
137+
}
138+
120139
Ok(StatusCode::NO_CONTENT)
121140
}
122141

@@ -148,6 +167,33 @@ async fn has_rev_dep(conn: &mut AsyncPgConnection, crate_id: i32) -> QueryResult
148167
Ok(rev_dep.is_some())
149168
}
150169

170+
/// Email template for notifying a crate owner about a crate being deleted.
171+
///
172+
/// The owner usually should be aware of the deletion since they initiated it,
173+
/// but this email can be helpful in detecting malicious account activity.
174+
#[derive(Debug, Clone)]
175+
struct CrateDeletionEmail<'a> {
176+
user: &'a str,
177+
krate: &'a str,
178+
}
179+
180+
impl Email for CrateDeletionEmail<'_> {
181+
fn subject(&self) -> String {
182+
format!("crates.io: Deleted \"{}\" crate", self.krate)
183+
}
184+
185+
fn body(&self) -> String {
186+
format!(
187+
"Hi {},
188+
189+
your \"{}\" crate has been deleted, per your request.
190+
191+
If you did not initiate this deletion, your account may have been compromised. Please contact us at [email protected].",
192+
self.user, self.krate
193+
)
194+
}
195+
}
196+
151197
#[cfg(test)]
152198
mod tests {
153199
use super::*;
@@ -186,6 +232,8 @@ mod tests {
186232
assert_eq!(response.status(), StatusCode::NO_CONTENT);
187233
assert!(response.body().is_empty());
188234

235+
assert_snapshot!(app.emails_snapshot().await);
236+
189237
// Assert that the crate no longer exists
190238
assert_crate_exists(&anon, "foo", false).await;
191239
assert!(!upstream.crate_exists("foo")?);
@@ -221,6 +269,8 @@ mod tests {
221269
assert_eq!(response.status(), StatusCode::NO_CONTENT);
222270
assert!(response.body().is_empty());
223271

272+
assert_snapshot!(app.emails_snapshot().await);
273+
224274
// Assert that the crate no longer exists
225275
assert_crate_exists(&anon, "foo", false).await;
226276
assert!(!upstream.crate_exists("foo")?);
@@ -256,6 +306,8 @@ mod tests {
256306
assert_eq!(response.status(), StatusCode::NO_CONTENT);
257307
assert!(response.body().is_empty());
258308

309+
assert_snapshot!(app.emails_snapshot().await);
310+
259311
// Assert that the crate no longer exists
260312
assert_crate_exists(&anon, "foo", false).await;
261313
assert!(!upstream.crate_exists("foo")?);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
---
2+
source: src/controllers/krate/delete.rs
3+
expression: app.emails_snapshot().await
4+
snapshot_kind: text
5+
---
6+
To: foo@example.com
7+
From: crates.io <noreply@crates.io>
8+
Subject: crates.io: Successfully published foo@1.0.0
9+
Content-Type: text/plain; charset=utf-8
10+
Content-Transfer-Encoding: quoted-printable
11+
12+
Hello foo!
13+
14+
A new version of the package foo (1.0.0) was published by your account (htt=
15+
ps://crates.io/users/foo) at [0000-00-00T00:00:00Z].
16+
17+
If you have questions or security concerns, you can contact us at help@crat=
18+
es.io. If you would like to stop receiving these security notifications, yo=
19+
u can disable them in your account settings.
20+
----------------------------------------
21+
22+
To: foo@example.com
23+
From: crates.io <noreply@crates.io>
24+
Subject: crates.io: Deleted "foo" crate
25+
Content-Type: text/plain; charset=utf-8
26+
Content-Transfer-Encoding: quoted-printable
27+
28+
Hi foo,
29+
30+
your "foo" crate has been deleted, per your request.
31+
32+
If you did not initiate this deletion, your account may have been compromis=
33+
ed. Please contact us at help@crates.io.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
---
2+
source: src/controllers/krate/delete.rs
3+
expression: app.emails_snapshot().await
4+
snapshot_kind: text
5+
---
6+
To: foo@example.com
7+
From: crates.io <noreply@crates.io>
8+
Subject: crates.io: Successfully published foo@1.0.0
9+
Content-Type: text/plain; charset=utf-8
10+
Content-Transfer-Encoding: quoted-printable
11+
12+
Hello foo!
13+
14+
A new version of the package foo (1.0.0) was published by your account (htt=
15+
ps://crates.io/users/foo) at [0000-00-00T00:00:00Z].
16+
17+
If you have questions or security concerns, you can contact us at help@crat=
18+
es.io. If you would like to stop receiving these security notifications, yo=
19+
u can disable them in your account settings.
20+
----------------------------------------
21+
22+
To: foo@example.com
23+
From: crates.io <noreply@crates.io>
24+
Subject: crates.io: Deleted "foo" crate
25+
Content-Type: text/plain; charset=utf-8
26+
Content-Transfer-Encoding: quoted-printable
27+
28+
Hi foo,
29+
30+
your "foo" crate has been deleted, per your request.
31+
32+
If you did not initiate this deletion, your account may have been compromis=
33+
ed. Please contact us at help@crates.io.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
---
2+
source: src/controllers/krate/delete.rs
3+
expression: app.emails_snapshot().await
4+
snapshot_kind: text
5+
---
6+
To: foo@example.com
7+
From: crates.io <noreply@crates.io>
8+
Subject: crates.io: Successfully published foo@1.0.0
9+
Content-Type: text/plain; charset=utf-8
10+
Content-Transfer-Encoding: quoted-printable
11+
12+
Hello foo!
13+
14+
A new version of the package foo (1.0.0) was published by your account (htt=
15+
ps://crates.io/users/foo) at [0000-00-00T00:00:00Z].
16+
17+
If you have questions or security concerns, you can contact us at help@crat=
18+
es.io. If you would like to stop receiving these security notifications, yo=
19+
u can disable them in your account settings.
20+
----------------------------------------
21+
22+
To: foo@example.com
23+
From: crates.io <noreply@crates.io>
24+
Subject: crates.io: Deleted "foo" crate
25+
Content-Type: text/plain; charset=utf-8
26+
Content-Transfer-Encoding: quoted-printable
27+
28+
Hi foo,
29+
30+
your "foo" crate has been deleted, per your request.
31+
32+
If you did not initiate this deletion, your account may have been compromis=
33+
ed. Please contact us at help@crates.io.

0 commit comments

Comments
 (0)