Report which email failed to import when the blob is too large (fixes #22)
This commit is contained in:
@@ -11,6 +11,7 @@ All notable changes to this project will be documented in this file. This projec
|
|||||||
### Fixed
|
### Fixed
|
||||||
- WebDAV import materialised the account root collection as a directory named after the account displayname (#18).
|
- WebDAV import materialised the account root collection as a directory named after the account displayname (#18).
|
||||||
- Report user friendly error message when `urn:ietf:params:jmap:principals` is not supported and no accountId is provided (#21).
|
- Report user friendly error message when `urn:ietf:params:jmap:principals` is not supported and no accountId is provided (#21).
|
||||||
|
- Report which email failed to import when the blob is too large (#22).
|
||||||
|
|
||||||
## [1.0.6] - 2026-07-12
|
## [1.0.6] - 2026-07-12
|
||||||
|
|
||||||
|
|||||||
@@ -27,6 +27,18 @@ pub fn blob_bytes(conn: &Connection, id: i64) -> Result<Option<Vec<u8>>, rusqlit
|
|||||||
.optional()
|
.optional()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn blob_len(conn: &Connection, id: i64) -> Result<Option<u64>, rusqlite::Error> {
|
||||||
|
conn.query_row(
|
||||||
|
"SELECT length(data) FROM blobs WHERE id = ?1",
|
||||||
|
params![id],
|
||||||
|
|row| {
|
||||||
|
let len: i64 = row.get(0)?;
|
||||||
|
Ok(len.max(0) as u64)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.optional()
|
||||||
|
}
|
||||||
|
|
||||||
pub fn gc_orphan_blobs(conn: &Connection) -> Result<usize, rusqlite::Error> {
|
pub fn gc_orphan_blobs(conn: &Connection) -> Result<usize, rusqlite::Error> {
|
||||||
conn.execute(
|
conn.execute(
|
||||||
"DELETE FROM blobs WHERE id NOT IN (
|
"DELETE FROM blobs WHERE id NOT IN (
|
||||||
@@ -77,6 +89,14 @@ mod tests {
|
|||||||
assert_eq!(blob_bytes(&c, 9999).unwrap(), None);
|
assert_eq!(blob_bytes(&c, 9999).unwrap(), None);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn blob_len_reports_byte_length() {
|
||||||
|
let c = mem();
|
||||||
|
let id = intern_blob(&c, b"payload").unwrap();
|
||||||
|
assert_eq!(blob_len(&c, id).unwrap(), Some(7));
|
||||||
|
assert_eq!(blob_len(&c, 9999).unwrap(), None);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn gc_keeps_referenced_and_reaps_orphans() {
|
fn gc_keeps_referenced_and_reaps_orphans() {
|
||||||
let c = mem();
|
let c = mem();
|
||||||
|
|||||||
@@ -101,6 +101,10 @@ impl<'a> Uploader<'a> {
|
|||||||
self.cache.remove(&local_id);
|
self.cache.remove(&local_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn blob_len(&self, local_id: i64) -> Option<u64> {
|
||||||
|
db::blobs::blob_len(self.conn, local_id).ok().flatten()
|
||||||
|
}
|
||||||
|
|
||||||
fn take_touched(&mut self) -> Vec<i64> {
|
fn take_touched(&mut self) -> Vec<i64> {
|
||||||
std::mem::take(&mut self.touched)
|
std::mem::take(&mut self.touched)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -143,6 +143,30 @@ fn build_keywords(row: &EmailRow) -> Map<String, Value> {
|
|||||||
kw
|
kw
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn blob_hint(uploader: &Uploader, row: &EmailRow) -> String {
|
||||||
|
let idx = index_from_json(&row.message_match);
|
||||||
|
let mut s = match idx.mids.first() {
|
||||||
|
Some(mid) => format!("message-id <{mid}>"),
|
||||||
|
None => "no message-id".to_owned(),
|
||||||
|
};
|
||||||
|
if let Some(len) = uploader.blob_len(row.blob_local_id) {
|
||||||
|
use std::fmt::Write;
|
||||||
|
let _ = write!(s, ", {}", crate::inspect::format_bytes(len));
|
||||||
|
}
|
||||||
|
s
|
||||||
|
}
|
||||||
|
|
||||||
|
fn size_note(e: &JmapError) -> &'static str {
|
||||||
|
if matches!(
|
||||||
|
e,
|
||||||
|
JmapError::RequestTooLarge | JmapError::SingleObjectTooLarge(_)
|
||||||
|
) {
|
||||||
|
"; exceeds the target server size limit, so this message is skipped and re-running will not migrate it"
|
||||||
|
} else {
|
||||||
|
""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn import_item(
|
fn import_item(
|
||||||
blob: String,
|
blob: String,
|
||||||
mids: Map<String, Value>,
|
mids: Map<String, Value>,
|
||||||
@@ -171,7 +195,8 @@ fn export_one(
|
|||||||
Some(m) => m,
|
Some(m) => m,
|
||||||
None => {
|
None => {
|
||||||
logger.warn(&format!(
|
logger.warn(&format!(
|
||||||
"email local {local_id} skipped: mailbox not on target"
|
"Email/import {cid} ({}) skipped: mailbox not on target",
|
||||||
|
blob_hint(uploader, row)
|
||||||
));
|
));
|
||||||
counts.failed += 1;
|
counts.failed += 1;
|
||||||
return;
|
return;
|
||||||
@@ -180,7 +205,11 @@ fn export_one(
|
|||||||
let blob = match uploader.upload_with(row.blob_local_id, "message/rfc822") {
|
let blob = match uploader.upload_with(row.blob_local_id, "message/rfc822") {
|
||||||
Ok(b) => b.0,
|
Ok(b) => b.0,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
logger.warn(&format!("email blob upload failed: {e}"));
|
logger.warn(&format!(
|
||||||
|
"Email/import {cid} ({}) blob upload failed: {e}{}",
|
||||||
|
blob_hint(uploader, row),
|
||||||
|
size_note(&e)
|
||||||
|
));
|
||||||
counts.failed += 1;
|
counts.failed += 1;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -197,11 +226,18 @@ fn export_one(
|
|||||||
retry_after_reupload(net, uploader, maps, &cid, row, counts, logger);
|
retry_after_reupload(net, uploader, maps, &cid, row, counts, logger);
|
||||||
}
|
}
|
||||||
Ok(SingleImport::NotCreated { detail, .. }) => {
|
Ok(SingleImport::NotCreated { detail, .. }) => {
|
||||||
logger.warn(&format!("Email/import {cid} failed: {detail}"));
|
logger.warn(&format!(
|
||||||
|
"Email/import {cid} ({}) failed: {detail}",
|
||||||
|
blob_hint(uploader, row)
|
||||||
|
));
|
||||||
counts.failed += 1;
|
counts.failed += 1;
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
logger.warn(&format!("Email/import {cid} send failed: {e}"));
|
logger.warn(&format!(
|
||||||
|
"Email/import {cid} ({}) send failed: {e}{}",
|
||||||
|
blob_hint(uploader, row),
|
||||||
|
size_note(&e)
|
||||||
|
));
|
||||||
counts.failed += 1;
|
counts.failed += 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -220,7 +256,11 @@ fn retry_after_reupload(
|
|||||||
let blob = match uploader.upload_with(row.blob_local_id, "message/rfc822") {
|
let blob = match uploader.upload_with(row.blob_local_id, "message/rfc822") {
|
||||||
Ok(b) => b.0,
|
Ok(b) => b.0,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
logger.warn(&format!("Email/import {cid}: blob re-upload failed: {e}"));
|
logger.warn(&format!(
|
||||||
|
"Email/import {cid} ({}) blob re-upload failed: {e}{}",
|
||||||
|
blob_hint(uploader, row),
|
||||||
|
size_note(&e)
|
||||||
|
));
|
||||||
counts.failed += 1;
|
counts.failed += 1;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -229,7 +269,8 @@ fn retry_after_reupload(
|
|||||||
Some(m) => m,
|
Some(m) => m,
|
||||||
None => {
|
None => {
|
||||||
logger.warn(&format!(
|
logger.warn(&format!(
|
||||||
"Email/import {cid} skipped: mailbox not on target"
|
"Email/import {cid} ({}) skipped: mailbox not on target",
|
||||||
|
blob_hint(uploader, row)
|
||||||
));
|
));
|
||||||
counts.failed += 1;
|
counts.failed += 1;
|
||||||
return;
|
return;
|
||||||
@@ -241,13 +282,16 @@ fn retry_after_reupload(
|
|||||||
Ok(SingleImport::Skipped) => counts.skipped += 1,
|
Ok(SingleImport::Skipped) => counts.skipped += 1,
|
||||||
Ok(SingleImport::NotCreated { detail, .. }) => {
|
Ok(SingleImport::NotCreated { detail, .. }) => {
|
||||||
logger.warn(&format!(
|
logger.warn(&format!(
|
||||||
"Email/import {cid} failed after blob re-upload: {detail}"
|
"Email/import {cid} ({}) failed after blob re-upload: {detail}",
|
||||||
|
blob_hint(uploader, row)
|
||||||
));
|
));
|
||||||
counts.failed += 1;
|
counts.failed += 1;
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
logger.warn(&format!(
|
logger.warn(&format!(
|
||||||
"Email/import {cid} send failed after blob re-upload: {e}"
|
"Email/import {cid} ({}) send failed after blob re-upload: {e}{}",
|
||||||
|
blob_hint(uploader, row),
|
||||||
|
size_note(&e)
|
||||||
));
|
));
|
||||||
counts.failed += 1;
|
counts.failed += 1;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user