1 parent
93762fe9ef
commit
96a32ea8fb
17 files changed
+91
-34
No files matched your search
@@ -1,5 +1,11 @@
|
|||||||
# RustBox changelog
|
# RustBox changelog
|
||||||
|
|
||||||
|
### 0.0.4
|
||||||
|
- Changed attachment left-click behavior to prefer in-app preview for supported files while keeping direct download for non-previewable formats.
|
||||||
|
- Added attachment right-click actions for explicit Preview and Download.
|
||||||
|
- When an attachment exceeds the preview limit, RustBox now offers to download it instead of stopping at a warning; known oversized files are detected before their contents are fetched for preview.
|
||||||
|
- Bumped the application/source release to `0.0.4`.
|
||||||
|
|
||||||
### 0.0.3
|
### 0.0.3
|
||||||
- Fixed reader toolbar dropdowns for Message details and Move/copy by moving popovers out of the horizontally clipped toolbar layer.
|
- Fixed reader toolbar dropdowns for Message details and Move/copy by moving popovers out of the horizontally clipped toolbar layer.
|
||||||
- Added Fastmail-, Apple Mail-, Yahoo Mail- and HEY-inspired styles plus Nord, and an optional per-account unlock for UI scaling on predefined styles.
|
- Added Fastmail-, Apple Mail-, Yahoo Mail- and HEY-inspired styles plus Nord, and an optional per-account unlock for UI scaling on predefined styles.
|
||||||
|
|||||||
Generated
+1
-1
@@ -2957,7 +2957,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustbox"
|
name = "rustbox"
|
||||||
version = "0.0.3"
|
version = "0.0.4"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-imap",
|
"async-imap",
|
||||||
"async-native-tls",
|
"async-native-tls",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "rustbox"
|
name = "rustbox"
|
||||||
version = "0.0.3"
|
version = "0.0.4"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
rust-version = "1.88"
|
rust-version = "1.88"
|
||||||
default-run = "rustbox"
|
default-run = "rustbox"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# RustBox attribution / notice
|
# RustBox attribution / notice
|
||||||
|
|
||||||
RustBox 0.0.3 is derived from the Oxi webmail project:
|
RustBox 0.0.4 is derived from the Oxi webmail project:
|
||||||
|
|
||||||
- Upstream: `c0h1b4/oxi`
|
- Upstream: `c0h1b4/oxi`
|
||||||
- Base snapshot: `7f1507c486b78cf33e54beb701dd460d3bd2bd5d`
|
- Base snapshot: `7f1507c486b78cf33e54beb701dd460d3bd2bd5d`
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# RustBox 0.0.3
|
# RustBox 0.0.4
|
||||||
|
|
||||||
RustBox is a webmail derived from `c0h1b4/oxi`. The consumed Oxi revision is recorded in `UPSTREAM_BASE`.
|
RustBox is a webmail derived from `c0h1b4/oxi`. The consumed Oxi revision is recorded in `UPSTREAM_BASE`.
|
||||||
|
|
||||||
@@ -71,7 +71,7 @@ If the mail host settings are omitted, RustBox uses `localhost` with IMAP `993`
|
|||||||
|
|
||||||
Mail-size limits are intentionally separate. `max_attachment_size_mb` applies to each raw uploaded file, while `max_message_size_mb` is checked against the final RFC822/MIME byte stream immediately before SMTP submission. The latter therefore includes message bodies, headers, MIME boundaries, base64 expansion, inline CID parts and OpenPGP transformations. The composer displays only an estimate; the server-side final-MIME check is authoritative.
|
Mail-size limits are intentionally separate. `max_attachment_size_mb` applies to each raw uploaded file, while `max_message_size_mb` is checked against the final RFC822/MIME byte stream immediately before SMTP submission. The latter therefore includes message bodies, headers, MIME boundaries, base64 expansion, inline CID parts and OpenPGP transformations. The composer displays only an estimate; the server-side final-MIME check is authoritative.
|
||||||
|
|
||||||
## Current 0.0.3 UI snapshot
|
## Current 0.0.4 UI snapshot
|
||||||
|
|
||||||
The internal baseline includes persistent appearance/layout preferences, draggable mail-pane splitters, action toasts, recipient autocomplete, attachment preview/context actions and client-side ZIP download in addition to the existing IMAP/Sieve/mail workflow. The authoritative completion scope remains `docs/RUSTBOX_PLAN.md`; incomplete items are tracked in `docs/IMPLEMENTATION_STATUS.md`.
|
The internal baseline includes persistent appearance/layout preferences, draggable mail-pane splitters, action toasts, recipient autocomplete, attachment preview/context actions and client-side ZIP download in addition to the existing IMAP/Sieve/mail workflow. The authoritative completion scope remains `docs/RUSTBOX_PLAN.md`; incomplete items are tracked in `docs/IMPLEMENTATION_STATUS.md`.
|
||||||
|
|
||||||
@@ -141,7 +141,7 @@ Initialize Git after extracting the release archive:
|
|||||||
```bash
|
```bash
|
||||||
git init
|
git init
|
||||||
git add .
|
git add .
|
||||||
git commit -m "RustBox 0.0.3 release"
|
git commit -m "RustBox 0.0.4 release"
|
||||||
./scripts/setup-upstream.sh
|
./scripts/setup-upstream.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
RustBox 0.0.3 source release
|
RustBox 0.0.4 source release
|
||||||
|
|
||||||
Runtime/source model:
|
Runtime/source model:
|
||||||
- one root Cargo package
|
- one root Cargo package
|
||||||
@@ -19,7 +19,10 @@ Oxi tracking:
|
|||||||
- Oxi frontend changes are reviewed and manually ported to src/web/
|
- Oxi frontend changes are reviewed and manually ported to src/web/
|
||||||
- docs/UI.md documents the browser UI/build conventions
|
- docs/UI.md documents the browser UI/build conventions
|
||||||
|
|
||||||
Current 0.0.3 release additions:
|
Current 0.0.4 release additions:
|
||||||
|
- attachment left-click defaults to safe in-app preview for supported types
|
||||||
|
- attachment right-click exposes explicit Preview and Download actions
|
||||||
|
- oversized preview attempts offer direct download instead of ending with a warning
|
||||||
- Polish composer, reader toolbar and per-message context-menu translation pass
|
- Polish composer, reader toolbar and per-message context-menu translation pass
|
||||||
- grouped reader message details menu for headers/source and transfer menu for move/copy
|
- grouped reader message details menu for headers/source and transfer menu for move/copy
|
||||||
- compact composer format selector moved to the lower-right opposite Send/Cancel
|
- compact composer format selector moved to the lower-right opposite Send/Cancel
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
# RustBox 0.0.3
|
# RustBox 0.0.4
|
||||||
host = "127.0.0.1"
|
host = "127.0.0.1"
|
||||||
port = 3001
|
port = 3001
|
||||||
environment = "production"
|
environment = "production"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# RustBox 0.0.3 - implementation status
|
# RustBox 0.0.4 - implementation status
|
||||||
|
|
||||||
This file tracks the internal `0.0.3` baseline. The user-approved `docs/RUSTBOX_PLAN.md` remains authoritative. A feature is only listed as implemented when working code exists in this source tree; partial items are called out explicitly.
|
This file tracks the internal `0.0.4` baseline. The user-approved `docs/RUSTBOX_PLAN.md` remains authoritative. A feature is only listed as implemented when working code exists in this source tree; partial items are called out explicitly.
|
||||||
|
|
||||||
## Implemented / wired in this working snapshot
|
## Implemented / wired in this working snapshot
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# RustBox 0.0.3 — remaining roadmap gaps
|
# RustBox 0.0.4 — remaining roadmap gaps
|
||||||
|
|
||||||
Snapshot after the OpenPGP/key-lifecycle and unbounded Sieve action/condition work in this iteration.
|
Snapshot after the OpenPGP/key-lifecycle and unbounded Sieve action/condition work in this iteration.
|
||||||
|
|
||||||
@@ -74,7 +74,7 @@ OpenPGP is functionally complete for the scope defined in `RUSTBOX_PLAN.md` §9:
|
|||||||
### Search / notifications / diagnostics — 4
|
### Search / notifications / diagnostics — 4
|
||||||
|
|
||||||
- [ ] Complete attachment content/metadata indexing policy and limits.
|
- [ ] Complete attachment content/metadata indexing policy and limits.
|
||||||
- [ ] Background/service-worker notifications while the page is inactive, if retained for 0.0.3.
|
- [ ] Background/service-worker notifications while the page is inactive, if retained for 0.0.4.
|
||||||
- [ ] Richer per-account notification policy beyond global/per-folder controls.
|
- [ ] Richer per-account notification policy beyond global/per-folder controls.
|
||||||
- [ ] Background task/queue status plus recent-error/repair workflows.
|
- [ ] Background task/queue status plus recent-error/repair workflows.
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
# RustBox 0.0.4
|
||||||
|
|
||||||
|
RustBox 0.0.4 improves attachment interaction in the message reader.
|
||||||
|
|
||||||
|
## Highlights
|
||||||
|
|
||||||
|
- Left-click previews supported attachment types in the in-app viewer.
|
||||||
|
- Right-click exposes explicit **Preview** and **Download** actions.
|
||||||
|
- Unsupported attachment types still download directly on left-click.
|
||||||
|
- Attachments that exceed the preview limit now offer **Download** instead of only showing a warning.
|
||||||
|
- Known oversized attachments are rejected before fetching their full payload for preview.
|
||||||
@@ -126,7 +126,7 @@ async fn https_get_address(
|
|||||||
format!("{host}:{port}")
|
format!("{host}:{port}")
|
||||||
};
|
};
|
||||||
let request = format!(
|
let request = format!(
|
||||||
"GET {target} HTTP/1.1\r\nHost: {host_header}\r\nUser-Agent: RustBox/0.0.3\r\nAccept: application/octet-stream, application/pgp-keys, text/plain\r\nConnection: close\r\n\r\n"
|
"GET {target} HTTP/1.1\r\nHost: {host_header}\r\nUser-Agent: RustBox/0.0.4\r\nAccept: application/octet-stream, application/pgp-keys, text/plain\r\nConnection: close\r\n\r\n"
|
||||||
);
|
);
|
||||||
timeout(duration, stream.write_all(request.as_bytes()))
|
timeout(duration, stream.write_all(request.as_bytes()))
|
||||||
.await
|
.await
|
||||||
|
|||||||
@@ -192,7 +192,7 @@ pub async fn export_ics(
|
|||||||
.map_err(|e| AppError::InternalError(format!("Database error: {e}")))?;
|
.map_err(|e| AppError::InternalError(format!("Database error: {e}")))?;
|
||||||
|
|
||||||
let mut body = String::from(
|
let mut body = String::from(
|
||||||
"BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//RustBox//Calendar 0.0.3//EN\r\nCALSCALE:GREGORIAN\r\n",
|
"BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//RustBox//Calendar 0.0.4//EN\r\nCALSCALE:GREGORIAN\r\n",
|
||||||
);
|
);
|
||||||
for event in &events {
|
for event in &events {
|
||||||
append_ics_event(&mut body, event);
|
append_ics_event(&mut body, event);
|
||||||
|
|||||||
@@ -230,7 +230,7 @@ async fn post_one_click_to_address(
|
|||||||
format!("{host}:{port}")
|
format!("{host}:{port}")
|
||||||
};
|
};
|
||||||
let request = format!(
|
let request = format!(
|
||||||
"POST {path} HTTP/1.1\r\nHost: {host_header}\r\nUser-Agent: RustBox/0.0.3\r\nContent-Type: application/x-www-form-urlencoded\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{ONE_CLICK_BODY}",
|
"POST {path} HTTP/1.1\r\nHost: {host_header}\r\nUser-Agent: RustBox/0.0.4\r\nContent-Type: application/x-www-form-urlencoded\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{ONE_CLICK_BODY}",
|
||||||
ONE_CLICK_BODY.len()
|
ONE_CLICK_BODY.len()
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -29,7 +29,7 @@ impl fmt::Display for SieveError {
|
|||||||
write!(f, "ManageSieve server rejected the command: {msg}")
|
write!(f, "ManageSieve server rejected the command: {msg}")
|
||||||
}
|
}
|
||||||
Self::Timeout => write!(f, "ManageSieve operation timed out"),
|
Self::Timeout => write!(f, "ManageSieve operation timed out"),
|
||||||
Self::ScriptTooLarge => write!(f, "Sieve script exceeds the RustBox 0.0.3 size limit"),
|
Self::ScriptTooLarge => write!(f, "Sieve script exceeds the RustBox 0.0.4 size limit"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -569,6 +569,11 @@
|
|||||||
"Recovered draft": "Odzyskany szkic",
|
"Recovered draft": "Odzyskany szkic",
|
||||||
"Forward as attachment": "Przekaż jako załącznik",
|
"Forward as attachment": "Przekaż jako załącznik",
|
||||||
"Open": "Otwórz",
|
"Open": "Otwórz",
|
||||||
|
"Preview": "Podgląd",
|
||||||
|
"Download": "Pobierz",
|
||||||
|
"Preview unavailable": "Podgląd niedostępny",
|
||||||
|
"This attachment is too large for preview. Download it instead?": "Ten załącznik jest za duży do podglądu. Pobrać go zamiast tego?",
|
||||||
|
"Preview is not available for this attachment type. Download it instead?": "Podgląd nie jest dostępny dla tego typu załącznika. Pobrać go zamiast tego?",
|
||||||
"Mark unread": "Oznacz jako nieprzeczytaną",
|
"Mark unread": "Oznacz jako nieprzeczytaną",
|
||||||
"Unflag": "Usuń oznaczenie",
|
"Unflag": "Usuń oznaczenie",
|
||||||
"Move to…": "Przenieś do…",
|
"Move to…": "Przenieś do…",
|
||||||
|
|||||||
@@ -326,10 +326,9 @@ function renderMessageDetail(detail) {
|
|||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
const filename = String(attachment.filename || "").toLowerCase();
|
const filename = String(attachment.filename || "").toLowerCase();
|
||||||
const contentType = String(attachment.content_type || attachment.contentType || "").toLowerCase();
|
const contentType = String(attachment.content_type || attachment.contentType || "").toLowerCase();
|
||||||
const actions = [
|
const actions = [];
|
||||||
{ label: "Open", action: () => openAttachment(detail.folder, detail.uid, attachment) },
|
if (attachmentCanPreview(attachment)) actions.push({ label: "Preview", action: () => openAttachment(detail.folder, detail.uid, attachment) });
|
||||||
{ label: "Download", action: () => downloadAttachment(detail.folder, detail.uid, attachment) },
|
actions.push({ label: "Download", action: () => downloadAttachment(detail.folder, detail.uid, attachment) });
|
||||||
];
|
|
||||||
if (contentType.includes("text/calendar") || filename.endsWith(".ics")) actions.push("separator", { label: "Add to calendar", action: () => importCalendarAttachment(detail.folder, detail.uid, attachment) });
|
if (contentType.includes("text/calendar") || filename.endsWith(".ics")) actions.push("separator", { label: "Add to calendar", action: () => importCalendarAttachment(detail.folder, detail.uid, attachment) });
|
||||||
if (contentType.includes("text/vcard") || contentType.includes("text/x-vcard") || filename.endsWith(".vcf")) actions.push("separator", { label: "Import contact", action: () => importVcardAttachment(detail.folder, detail.uid, attachment) });
|
if (contentType.includes("text/vcard") || contentType.includes("text/x-vcard") || filename.endsWith(".vcf")) actions.push("separator", { label: "Import contact", action: () => importVcardAttachment(detail.folder, detail.uid, attachment) });
|
||||||
if ((detail.attachments || []).length > 1) actions.push("separator", { label: "Download all as ZIP", action: () => downloadAllAttachmentsZip(detail) });
|
if ((detail.attachments || []).length > 1) actions.push("separator", { label: "Download all as ZIP", action: () => downloadAllAttachmentsZip(detail) });
|
||||||
@@ -514,27 +513,59 @@ function attachmentMime(attachment, blob = null) {
|
|||||||
return String(blob?.type || attachment?.content_type || attachment?.contentType || "application/octet-stream").toLowerCase();
|
return String(blob?.type || attachment?.content_type || attachment?.contentType || "application/octet-stream").toLowerCase();
|
||||||
}
|
}
|
||||||
|
|
||||||
function attachmentCanPreview(attachment) {
|
function attachmentPreviewLimitBytes(attachment) {
|
||||||
const mime = attachmentMime(attachment);
|
const mime = attachmentMime(attachment);
|
||||||
const filename = String(attachment?.filename || "").toLowerCase();
|
const filename = String(attachment?.filename || "").toLowerCase();
|
||||||
return mime.startsWith("image/")
|
if (mime.startsWith("text/") || /\.(txt|log|md|csv|html?|xhtml)$/i.test(filename)) return 5 * 1024 * 1024;
|
||||||
|| mime === "application/pdf"
|
if (mime.startsWith("image/") || mime === "application/pdf" || mime.startsWith("video/") || /\.(pdf|png|jpe?g|gif|webp|mp4|webm|ogv|mov)$/i.test(filename)) return 30 * 1024 * 1024;
|
||||||
|| mime.startsWith("text/")
|
return 0;
|
||||||
|| mime.startsWith("video/")
|
}
|
||||||
|| /\.(pdf|png|jpe?g|gif|webp|txt|log|md|csv|html?|xhtml|mp4|webm|ogv|mov)$/i.test(filename);
|
|
||||||
|
function attachmentCanPreview(attachment) {
|
||||||
|
return attachmentPreviewLimitBytes(attachment) > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function attachmentExceedsPreviewLimit(attachment, actualSize = null) {
|
||||||
|
const limit = attachmentPreviewLimitBytes(attachment);
|
||||||
|
if (!limit) return false;
|
||||||
|
const size = Number(actualSize ?? attachment?.size ?? 0);
|
||||||
|
return Number.isFinite(size) && size > limit;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function offerAttachmentDownload(folder, uid, attachment, reason) {
|
||||||
|
const confirmed = await appConfirm(trText(reason), {
|
||||||
|
title: trText("Preview unavailable"),
|
||||||
|
confirmLabel: trText("Download"),
|
||||||
|
});
|
||||||
|
if (confirmed) await downloadAttachment(folder, uid, attachment);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function openAttachment(folder, uid, attachment) {
|
async function openAttachment(folder, uid, attachment) {
|
||||||
try {
|
try {
|
||||||
|
if (!attachmentCanPreview(attachment)) {
|
||||||
|
await offerAttachmentDownload(folder, uid, attachment, "Preview is not available for this attachment type. Download it instead?");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (attachmentExceedsPreviewLimit(attachment)) {
|
||||||
|
await offerAttachmentDownload(folder, uid, attachment, "This attachment is too large for preview. Download it instead?");
|
||||||
|
return;
|
||||||
|
}
|
||||||
const blob = await fetchAttachmentBlob(folder, uid, attachment);
|
const blob = await fetchAttachmentBlob(folder, uid, attachment);
|
||||||
const mime = attachmentMime(attachment, blob);
|
const mime = attachmentMime(attachment, blob);
|
||||||
const filename = String(attachment?.filename || "").toLowerCase();
|
const filename = String(attachment?.filename || "").toLowerCase();
|
||||||
const isHtml = mime.includes("html") || /\.(html?|xhtml)$/i.test(filename);
|
const isHtml = mime.includes("html") || /\.(html?|xhtml)$/i.test(filename);
|
||||||
const isImage = mime.startsWith("image/") || /\.(png|jpe?g|gif|webp)$/i.test(filename);
|
const isImage = mime.startsWith("image/") || /\.(png|jpe?g|gif|webp)$/i.test(filename);
|
||||||
const isVideo = mime.startsWith("video/") || /\.(mp4|webm|ogv|mov)$/i.test(filename);
|
const isVideo = mime.startsWith("video/") || /\.(mp4|webm|ogv|mov)$/i.test(filename);
|
||||||
const safe = attachmentCanPreview({ ...attachment, content_type: mime });
|
const previewAttachment = { ...attachment, content_type: mime };
|
||||||
if (!safe) return toast("Preview is not available for this attachment type", "warning");
|
const safe = attachmentCanPreview(previewAttachment);
|
||||||
if (blob.size > 30 * 1024 * 1024) return toast("Attachment is too large for preview", "warning");
|
if (!safe) {
|
||||||
|
await offerAttachmentDownload(folder, uid, attachment, "Preview is not available for this attachment type. Download it instead?");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (attachmentExceedsPreviewLimit(previewAttachment, blob.size)) {
|
||||||
|
await offerAttachmentDownload(folder, uid, attachment, "This attachment is too large for preview. Download it instead?");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const dialog = el("dialog", "dialog attachment-preview-dialog");
|
const dialog = el("dialog", "dialog attachment-preview-dialog");
|
||||||
const card = el("div", "dialog-card attachment-preview-card");
|
const card = el("div", "dialog-card attachment-preview-card");
|
||||||
@@ -570,7 +601,6 @@ async function openAttachment(folder, uid, attachment) {
|
|||||||
video.preload = "metadata";
|
video.preload = "metadata";
|
||||||
body.append(video);
|
body.append(video);
|
||||||
} else if (isHtml) {
|
} else if (isHtml) {
|
||||||
if (blob.size > 5 * 1024 * 1024) return toast("HTML attachment is too large for inline preview", "warning");
|
|
||||||
const frame = document.createElement("iframe");
|
const frame = document.createElement("iframe");
|
||||||
frame.setAttribute("sandbox", "");
|
frame.setAttribute("sandbox", "");
|
||||||
frame.referrerPolicy = "no-referrer";
|
frame.referrerPolicy = "no-referrer";
|
||||||
@@ -579,7 +609,6 @@ async function openAttachment(folder, uid, attachment) {
|
|||||||
frame.srcdoc = `<!doctype html><html><head>${csp}<meta name="color-scheme" content="light"><style>body{margin:0;padding:20px;color:#202622;background:#fff;font:14px/1.55 system-ui,sans-serif}img{max-width:100%;height:auto}pre{white-space:pre-wrap}</style></head><body>${safeHtml}</body></html>`;
|
frame.srcdoc = `<!doctype html><html><head>${csp}<meta name="color-scheme" content="light"><style>body{margin:0;padding:20px;color:#202622;background:#fff;font:14px/1.55 system-ui,sans-serif}img{max-width:100%;height:auto}pre{white-space:pre-wrap}</style></head><body>${safeHtml}</body></html>`;
|
||||||
body.append(frame);
|
body.append(frame);
|
||||||
} else {
|
} else {
|
||||||
if (blob.size > 5 * 1024 * 1024) return toast("Text attachment is too large for inline preview", "warning");
|
|
||||||
const pre = document.createElement("pre");
|
const pre = document.createElement("pre");
|
||||||
pre.textContent = await blob.text();
|
pre.textContent = await blob.text();
|
||||||
body.append(pre);
|
body.append(pre);
|
||||||
|
|||||||
@@ -102,6 +102,9 @@ def test_modular_js_contract():
|
|||||||
assert "presetScaleUnlocked" in app
|
assert "presetScaleUnlocked" in app
|
||||||
assert "positionToolbarMenu" in app
|
assert "positionToolbarMenu" in app
|
||||||
assert "progressToast" in app
|
assert "progressToast" in app
|
||||||
|
assert "attachmentPreviewLimitBytes" in app
|
||||||
|
assert 'label: "Preview"' in app
|
||||||
|
assert "This attachment is too large for preview. Download it instead?" in app
|
||||||
assert "appearancePresets" not in app
|
assert "appearancePresets" not in app
|
||||||
assert "applyAppearancePreset" not in app
|
assert "applyAppearancePreset" not in app
|
||||||
assert "src/web/js/manifest.txt" in (ROOT / "README.md").read_text()
|
assert "src/web/js/manifest.txt" in (ROOT / "README.md").read_text()
|
||||||
@@ -116,7 +119,7 @@ def test_backend_skin_contract_and_version():
|
|||||||
for value in ["rustbox", "roundcube-classic", "roundcube-larry", "roundcube-elastic", "clean", "gmail", "proton", "zoho", "outlook", "minimal", "graphite", "high-contrast", "fastmail", "apple-mail", "yahoo", "hey", "nord"]:
|
for value in ["rustbox", "roundcube-classic", "roundcube-larry", "roundcube-elastic", "clean", "gmail", "proton", "zoho", "outlook", "minimal", "graphite", "high-contrast", "fastmail", "apple-mail", "yahoo", "hey", "nord"]:
|
||||||
assert f'"{value}"' in db_source
|
assert f'"{value}"' in db_source
|
||||||
cargo = (ROOT / "Cargo.toml").read_text()
|
cargo = (ROOT / "Cargo.toml").read_text()
|
||||||
assert 'version = "0.0.3"' in cargo
|
assert 'version = "0.0.4"' in cargo
|
||||||
config = (ROOT / "src/config.rs").read_text()
|
config = (ROOT / "src/config.rs").read_text()
|
||||||
assert "pub default_theme: String" in config
|
assert "pub default_theme: String" in config
|
||||||
assert '"system" | "dark" | "light"' in config
|
assert '"system" | "dark" | "light"' in config
|
||||||
@@ -138,7 +141,7 @@ def test_mobile_003_contract():
|
|||||||
assert ".message-meta.addresses-expanded" in responsive
|
assert ".message-meta.addresses-expanded" in responsive
|
||||||
pl = json.loads((ROOT / "src/web/i18n/pl.json").read_text())
|
pl = json.loads((ROOT / "src/web/i18n/pl.json").read_text())
|
||||||
assert len(pl.get("_text", {})) >= 450
|
assert len(pl.get("_text", {})) >= 450
|
||||||
for key in ["Rule enabled", "Stop after this rule", "Calendar", "Message headers", "Message source", "Forward as attachment", "Show headers", "Show source", "Write a message"]:
|
for key in ["Rule enabled", "Stop after this rule", "Calendar", "Message headers", "Message source", "Forward as attachment", "Show headers", "Show source", "Write a message", "Preview", "Download", "Preview unavailable"]:
|
||||||
assert key in pl["_text"]
|
assert key in pl["_text"]
|
||||||
for key in ["reply", "reply_all", "forward", "message_details", "move_copy", "move_to", "copy_to"]:
|
for key in ["reply", "reply_all", "forward", "message_details", "move_copy", "move_to", "copy_to"]:
|
||||||
assert key in pl
|
assert key in pl
|
||||||
|
|||||||
Reference in new issue
Block a user