| 143 |
143 |
|
.await
|
| 144 |
144 |
|
.map_err(|e| format!("Email ID migration failed: {e}"))?;
|
| 145 |
145 |
|
|
|
146 |
+ |
// Move any plaintext passwords left by older versions into the keychain.
|
|
147 |
+ |
scrub_legacy_email_passwords(&pool).await;
|
|
148 |
+ |
|
| 146 |
149 |
|
// Ensure desktop user exists (single-user mode)
|
| 147 |
150 |
|
ensure_desktop_user_exists(&pool).await?;
|
| 148 |
151 |
|
|
| 211 |
214 |
|
}
|
| 212 |
215 |
|
}
|
| 213 |
216 |
|
|
|
217 |
+ |
/// One-time scrub: move any plaintext password still in the `email_accounts`
|
|
218 |
+ |
/// column into the OS keychain, then blank the column. New accounts already
|
|
219 |
+ |
/// store secrets keychain-only (the column is `""`); this cleans up accounts
|
|
220 |
+ |
/// created by older versions so plaintext secrets don't linger in the DB file.
|
|
221 |
+ |
async fn scrub_legacy_email_passwords(pool: &SqlitePool) {
|
|
222 |
+ |
let rows: Vec<(String, String)> =
|
|
223 |
+ |
match sqlx::query_as("SELECT id, password FROM email_accounts WHERE password != ''")
|
|
224 |
+ |
.fetch_all(pool)
|
|
225 |
+ |
.await
|
|
226 |
+ |
{
|
|
227 |
+ |
Ok(rows) => rows,
|
|
228 |
+ |
Err(e) => {
|
|
229 |
+ |
warn!("Could not scan for legacy email passwords: {e}");
|
|
230 |
+ |
return;
|
|
231 |
+ |
}
|
|
232 |
+ |
};
|
|
233 |
+ |
|
|
234 |
+ |
for (id, password) in rows {
|
|
235 |
+ |
let Ok(uuid) = uuid::Uuid::parse_str(&id) else {
|
|
236 |
+ |
continue;
|
|
237 |
+ |
};
|
|
238 |
+ |
// Store in the keychain only if it isn't already there; if that fails,
|
|
239 |
+ |
// leave the column intact and retry on a future launch.
|
|
240 |
+ |
if crate::oauth::CredentialStore::get_password(uuid).is_none()
|
|
241 |
+ |
&& let Err(e) = crate::oauth::CredentialStore::store_password(uuid, &password)
|
|
242 |
+ |
{
|
|
243 |
+ |
warn!("Failed to migrate email password to keychain for {id}: {e}");
|
|
244 |
+ |
continue;
|
|
245 |
+ |
}
|
|
246 |
+ |
if let Err(e) = sqlx::query("UPDATE email_accounts SET password = '' WHERE id = ?")
|
|
247 |
+ |
.bind(&id)
|
|
248 |
+ |
.execute(pool)
|
|
249 |
+ |
.await
|
|
250 |
+ |
{
|
|
251 |
+ |
warn!("Failed to blank legacy email password column for {id}: {e}");
|
|
252 |
+ |
} else {
|
|
253 |
+ |
info!("Migrated a legacy email password to the keychain");
|
|
254 |
+ |
}
|
|
255 |
+ |
}
|
|
256 |
+ |
}
|
|
257 |
+ |
|
| 214 |
258 |
|
/// Load a SyncKit API key from the keychain, migrating from plaintext file if needed.
|
| 215 |
259 |
|
fn load_api_key(data_dir: &std::path::Path) -> Option<String> {
|
| 216 |
260 |
|
// Migrate plaintext file to keychain (one-time)
|