|
1 | 1 | use crate::app::AppState; |
2 | 2 | use crate::auth::AuthCheck; |
3 | 3 | use crate::controllers::krate::CratePath; |
| 4 | +use crate::email::Email; |
4 | 5 | use crate::models::{NewDeletedCrate, Rights}; |
5 | 6 | use crate::schema::{crate_downloads, crates, dependencies}; |
6 | 7 | use crate::util::errors::{custom, AppResult, BoxedAppError}; |
@@ -80,6 +81,7 @@ pub async fn delete_crate(path: CratePath, parts: Parts, app: AppState) -> AppRe |
80 | 81 | } |
81 | 82 | } |
82 | 83 |
|
| 84 | + let crate_name = krate.name.clone(); |
83 | 85 | conn.transaction(|conn| { |
84 | 86 | async move { |
85 | 87 | diesel::delete(crates::table.find(krate.id)) |
@@ -117,6 +119,23 @@ pub async fn delete_crate(path: CratePath, parts: Parts, app: AppState) -> AppRe |
117 | 119 | }) |
118 | 120 | .await?; |
119 | 121 |
|
| 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 | + |
120 | 139 | Ok(StatusCode::NO_CONTENT) |
121 | 140 | } |
122 | 141 |
|
@@ -148,6 +167,33 @@ async fn has_rev_dep(conn: &mut AsyncPgConnection, crate_id: i32) -> QueryResult |
148 | 167 | Ok(rev_dep.is_some()) |
149 | 168 | } |
150 | 169 |
|
| 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 | + |
151 | 197 | #[cfg(test)] |
152 | 198 | mod tests { |
153 | 199 | use super::*; |
@@ -186,6 +232,8 @@ mod tests { |
186 | 232 | assert_eq!(response.status(), StatusCode::NO_CONTENT); |
187 | 233 | assert!(response.body().is_empty()); |
188 | 234 |
|
| 235 | + assert_snapshot!(app.emails_snapshot().await); |
| 236 | + |
189 | 237 | // Assert that the crate no longer exists |
190 | 238 | assert_crate_exists(&anon, "foo", false).await; |
191 | 239 | assert!(!upstream.crate_exists("foo")?); |
@@ -221,6 +269,8 @@ mod tests { |
221 | 269 | assert_eq!(response.status(), StatusCode::NO_CONTENT); |
222 | 270 | assert!(response.body().is_empty()); |
223 | 271 |
|
| 272 | + assert_snapshot!(app.emails_snapshot().await); |
| 273 | + |
224 | 274 | // Assert that the crate no longer exists |
225 | 275 | assert_crate_exists(&anon, "foo", false).await; |
226 | 276 | assert!(!upstream.crate_exists("foo")?); |
@@ -256,6 +306,8 @@ mod tests { |
256 | 306 | assert_eq!(response.status(), StatusCode::NO_CONTENT); |
257 | 307 | assert!(response.body().is_empty()); |
258 | 308 |
|
| 309 | + assert_snapshot!(app.emails_snapshot().await); |
| 310 | + |
259 | 311 | // Assert that the crate no longer exists |
260 | 312 | assert_crate_exists(&anon, "foo", false).await; |
261 | 313 | assert!(!upstream.crate_exists("foo")?); |
|
0 commit comments