feat: address the CodeRabbit review on the mailbox allowance work by reserving the mailbox slot inside the account insert transaction under a per-organization lock so concurrent connects cannot both take the last one, resolving the storage limit under the quota lock so a plan change cannot be raced past, deleting refused attachment objects on a context that outlives the request, settling already-connected rows before the bulk batch spends allowance, leaving password columns out of the failed-rows CSV, stopping click propagation from the portalled allowance dialog, and counting bulk progress once

This commit is contained in:
Matthew Meszaros
2026-09-04 21:11:25 -07:00
parent 3596a9770f
commit bf46839fb7
14 changed files with 193 additions and 76 deletions
@@ -286,7 +286,9 @@ export default function BulkConnectPanel({
const skipped = rows.filter((r) => r.status === "skipped").length;
const failed = rows.filter((r) => r.status === "failed").length;
const pending = rows.filter((r) => r.status === "pending").length;
const total = ready + connected + skipped + failed + pending;
// `ready` already includes pending rows, so count the untouched ones alone.
const notStarted = rows.filter((r) => r.status === "ready").length;
const total = notStarted + connected + skipped + failed + pending;
if (step === "run") {
const done = connected + skipped + failed;
@@ -406,7 +408,7 @@ export default function BulkConnectPanel({
<button
type="button"
onClick={downloadFailed}
title="The rows as you uploaded them, including passwords, plus an error column"
title="The rows as you uploaded them, minus passwords, plus an error column"
className="ml-auto h-6 px-2 rounded text-[11px] text-slate-700 hover:text-slate-900 hover:bg-slate-100 inline-flex items-center gap-1 transition-colors"
>
<DownloadIcon className="w-3 h-3" />
@@ -444,7 +446,7 @@ export default function BulkConnectPanel({
<div className="px-4 py-2.5 border-t border-slate-200 bg-slate-50/60 flex items-center gap-2 min-w-0 sticky bottom-0">
<div className="text-[11px] text-slate-500 min-w-0 flex-1 truncate">
{failedRows.length > 0
? "Fix the failed rows in the download and upload that file; connected ones are skipped."
? "Retry keeps the passwords in memory. The download leaves them out; add them back before re-uploading."
: "Warmup and sync start on every new mailbox right away."}
</div>
{(canRetry || notSent > 0) && (
@@ -8,8 +8,10 @@
// pointed at a plan instead, because that is its only path to more.
//
// Layered above the connect modal: it marks itself data-floating so the modal
// underneath ignores Escape while it is up, and stops mousedown so the
// modal's backdrop handler never sees a click inside it.
// underneath ignores Escape while it is up, and stops both mousedown and
// click. The portal keeps it in the modal's React tree, so without that a
// click anywhere in here would reach the modal's backdrop handler and close
// the whole connect flow.
import React from "react";
import { createPortal } from "react-dom";
@@ -121,6 +123,7 @@ export default function MailboxAllowanceDialog({
e.stopPropagation();
onClose();
}}
onClick={(e) => e.stopPropagation()}
className="fixed inset-0 z-[140] flex items-center justify-center bg-slate-900/40 backdrop-blur-[2px] px-4"
>
<motion.div
@@ -206,10 +206,16 @@ export function downloadTemplate() {
downloadBlob(new Blob(["" + csv], { type: "text/csv;charset=utf-8" }), "warmbly-mailboxes-template.csv");
}
/** The rows that did not connect, with the user's own columns plus the reason. */
/** Columns never written to a download: a CSV in a Downloads folder is
* synced, backed up and shared far more than the user intends. */
const SECRET_COLUMNS = new Set(["password", "smtp_password", "imap_password"]);
/** The rows that did not connect, with the user's own columns (minus
* passwords) plus the reason. */
export function failedRowsCSV(rows: BulkRow[], columns: string[]): string {
const header = [...columns, "error"];
const body = rows.map((r) => [...columns.map((c) => r.raw[c] ?? ""), r.problem ?? r.message ?? r.code ?? ""]);
const keep = columns.filter((c) => !SECRET_COLUMNS.has(c));
const header = [...keep, "error"];
const body = rows.map((r) => [...keep.map((c) => r.raw[c] ?? ""), r.problem ?? r.message ?? r.code ?? ""]);
return Papa.unparse([header, ...body]);
}