Skip to content

Commit 1e50d69

Browse files
committed
controllers/krate/delete: Send deletion notification email after successful deletion
1 parent ddac704 commit 1e50d69

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};
@@ -79,6 +80,7 @@ pub async fn delete_crate(path: CratePath, parts: Parts, app: AppState) -> AppRe
7980
}
8081
}
8182

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

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

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

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

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

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

308+
assert_snapshot!(app.emails_snapshot().await);
309+
258310
// Assert that the crate no longer exists
259311
assert_crate_exists(&anon, "foo", false).await;
260312
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+
this is a confirmation email for the deletion of your "foo" crate.
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+
this is a confirmation email for the deletion of your "foo" crate.
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+
this is a confirmation email for the deletion of your "foo" crate.
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)