Problem
Sending an email via JMAP completes normally when the body is plain (no attachment), but hangs indefinitely if any attachment is included. No error is surfaced, no send-failed event fires, and the op-failed hook is never hit. The compose window closes (the sync part of send_message returns Ok), but the background send task blocks forever.
Found while testing the attachment-token flow in #78; pre-existing (that PR does not touch the JMAP send path).
Where to look
src-tauri/src/mail/jmap.rs::JmapConnection::send_email
The function does two HTTP round trips:
POST /upload/{accountId} with Content-Type: message/rfc822 and the full raw MIME bytes as the body.
- A standard JMAP
Email/import + EmailSubmission/set call using the returned blobId.
The attachment-less case is fast because the body is tiny (<2 KB). With a ~70 KB PNG embedded, the raw message is on the order of ~100 KB after base64 — still small, but enough that the behaviour diverges.
The reqwest::Client used has no timeout configured, so a stuck upload stays stuck indefinitely instead of returning an error.
Diagnostics needed
Before fixing, confirm where the hang is:
- Wrap the upload
send().await in a tokio::time::timeout (30–60 s) so a hang becomes a visible error rather than silent. Surface it via the existing op-failed path.
- Add
log::debug! breadcrumbs around the upload:
JMAP upload: start, {bytes} bytes to {url}
JMAP upload: request sent, awaiting response
JMAP upload: response received, status={status}, elapsed={ms}
This will tell us whether the client can't flush the body, the server never responds, or the JSON response never arrives.
Possible root causes (speculation until logs are in)
- Server-side: the JMAP provider may reject or silently drop an
rfc822 upload of this size; the connection stays open but nothing is written back. A sane timeout fixes the UX either way.
- reqwest keep-alive reuse: the same
http client is used for both the auth/session discovery and the upload. If the prior connection is in a half-closed state the upload could stall. Confirmable by forcing a fresh connection for the upload.
- Content-Length vs chunked:
body(Vec<u8>) sets Content-Length correctly; unlikely but worth a packet capture if the above two don't explain it.
Acceptance
- Sending a JMAP message with an attachment either succeeds or surfaces a concrete error within a reasonable timeout window (default request timeout on the JMAP client, e.g. 60 s).
- Log breadcrumbs around the upload make future reports of this shape diagnosable from the log alone.
Problem
Sending an email via JMAP completes normally when the body is plain (no attachment), but hangs indefinitely if any attachment is included. No error is surfaced, no
send-failedevent fires, and theop-failedhook is never hit. The compose window closes (the sync part ofsend_messagereturnsOk), but the background send task blocks forever.Found while testing the attachment-token flow in #78; pre-existing (that PR does not touch the JMAP send path).
Where to look
src-tauri/src/mail/jmap.rs::JmapConnection::send_emailThe function does two HTTP round trips:
POST /upload/{accountId}withContent-Type: message/rfc822and the full raw MIME bytes as the body.Email/import+EmailSubmission/setcall using the returnedblobId.The attachment-less case is fast because the body is tiny (<2 KB). With a ~70 KB PNG embedded, the raw message is on the order of ~100 KB after base64 — still small, but enough that the behaviour diverges.
The
reqwest::Clientused has no timeout configured, so a stuck upload stays stuck indefinitely instead of returning an error.Diagnostics needed
Before fixing, confirm where the hang is:
send().awaitin atokio::time::timeout(30–60 s) so a hang becomes a visible error rather than silent. Surface it via the existingop-failedpath.log::debug!breadcrumbs around the upload:JMAP upload: start, {bytes} bytes to {url}JMAP upload: request sent, awaiting responseJMAP upload: response received, status={status}, elapsed={ms}This will tell us whether the client can't flush the body, the server never responds, or the JSON response never arrives.
Possible root causes (speculation until logs are in)
rfc822upload of this size; the connection stays open but nothing is written back. A sane timeout fixes the UX either way.httpclient is used for both the auth/session discovery and the upload. If the prior connection is in a half-closed state the upload could stall. Confirmable by forcing a fresh connection for the upload.body(Vec<u8>)setsContent-Lengthcorrectly; unlikely but worth a packet capture if the above two don't explain it.Acceptance