@bobfrankston/mailx 1.0.457 → 1.0.459
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/build-icon-ico.js +262 -38
- package/bin/mailx.js +28 -1
- package/bin/mailx.js.map +1 -1
- package/bin/mailx.ts +27 -1
- package/client/app.js +27 -1
- package/client/app.js.map +1 -1
- package/client/icon.ico +0 -0
- package/package.json +1 -1
package/bin/build-icon-ico.js
CHANGED
|
@@ -1,17 +1,25 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
3
|
+
* Build client/icon.ico from client/icon.png with proper format coverage:
|
|
4
|
+
* - 16, 32, 48, 64, 128 px entries: BMP-DIB (BI_RGB) — required by the
|
|
5
|
+
* Windows Shell taskbar/AUMID code path. PNG-in-ICO at small sizes is
|
|
6
|
+
* silently rejected, leaving the taskbar showing the generic "document"
|
|
7
|
+
* icon (the bug behind "icon shows as a piece of paper").
|
|
8
|
+
* - 256 px entry: PNG (Vista+ accepts it; smaller file than the 256 KB
|
|
9
|
+
* a 256x256 BMP DIB would be).
|
|
7
10
|
*
|
|
8
11
|
* Run: node bin/build-icon-ico.js
|
|
9
12
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
13
|
+
* Source: client/icon.png. Re-run whenever the source PNG changes.
|
|
14
|
+
*
|
|
15
|
+
* Hand-rolled — no image deps available in the workspace. Decodes 8-bit
|
|
16
|
+
* RGBA / RGB non-interlaced PNG (which is what every standard tool emits),
|
|
17
|
+
* downsamples by box-averaging, encodes BMP DIBs with the trailing AND mask
|
|
18
|
+
* the ICO format requires.
|
|
12
19
|
*/
|
|
13
20
|
import fs from "node:fs";
|
|
14
21
|
import path from "node:path";
|
|
22
|
+
import zlib from "node:zlib";
|
|
15
23
|
|
|
16
24
|
const root = path.resolve(import.meta.dirname, "..");
|
|
17
25
|
const src = path.join(root, "client", "icon.png");
|
|
@@ -22,38 +30,254 @@ if (!fs.existsSync(src)) {
|
|
|
22
30
|
process.exit(1);
|
|
23
31
|
}
|
|
24
32
|
|
|
25
|
-
const
|
|
33
|
+
const pngBuf = fs.readFileSync(src);
|
|
26
34
|
|
|
27
|
-
//
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
35
|
+
// ── PNG decoder (8-bit RGBA / RGB, no interlace) ─────────────────────────
|
|
36
|
+
function decodePNG(buf) {
|
|
37
|
+
const sigExpected = [0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A];
|
|
38
|
+
if (buf.length < 8 || sigExpected.some((b, i) => buf[i] !== b)) {
|
|
39
|
+
throw new Error("not a PNG (bad signature)");
|
|
40
|
+
}
|
|
41
|
+
let pos = 8;
|
|
42
|
+
let width = 0, height = 0, bitDepth = 0, colorType = 0;
|
|
43
|
+
const idat = [];
|
|
44
|
+
while (pos < buf.length) {
|
|
45
|
+
const len = buf.readUInt32BE(pos); pos += 4;
|
|
46
|
+
const type = buf.toString("ascii", pos, pos + 4); pos += 4;
|
|
47
|
+
const data = buf.subarray(pos, pos + len); pos += len + 4; // skip CRC
|
|
48
|
+
if (type === "IHDR") {
|
|
49
|
+
width = data.readUInt32BE(0);
|
|
50
|
+
height = data.readUInt32BE(4);
|
|
51
|
+
bitDepth = data.readUInt8(8);
|
|
52
|
+
colorType = data.readUInt8(9);
|
|
53
|
+
if (data.readUInt8(12) !== 0) {
|
|
54
|
+
throw new Error("interlaced PNG not supported");
|
|
55
|
+
}
|
|
56
|
+
} else if (type === "IDAT") {
|
|
57
|
+
idat.push(data);
|
|
58
|
+
} else if (type === "IEND") {
|
|
59
|
+
break;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
if (bitDepth !== 8 && bitDepth !== 16) {
|
|
63
|
+
throw new Error(`bit depth ${bitDepth} not supported (need 8 or 16)`);
|
|
64
|
+
}
|
|
65
|
+
if (colorType !== 6 && colorType !== 2) {
|
|
66
|
+
throw new Error(`color type ${colorType} not supported (need 2=RGB or 6=RGBA)`);
|
|
67
|
+
}
|
|
68
|
+
const samplesPerPixel = colorType === 6 ? 4 : 3;
|
|
69
|
+
const bytesPerSample = bitDepth / 8; // 1 or 2
|
|
70
|
+
const bpp = samplesPerPixel * bytesPerSample;
|
|
71
|
+
const inflated = zlib.inflateSync(Buffer.concat(idat));
|
|
72
|
+
const stride = width * bpp;
|
|
73
|
+
const rgba = Buffer.alloc(width * height * 4);
|
|
74
|
+
let inPos = 0;
|
|
75
|
+
let prev = Buffer.alloc(stride);
|
|
76
|
+
let cur = Buffer.alloc(stride);
|
|
77
|
+
for (let y = 0; y < height; y++) {
|
|
78
|
+
const filter = inflated.readUInt8(inPos); inPos += 1;
|
|
79
|
+
inflated.copy(cur, 0, inPos, inPos + stride); inPos += stride;
|
|
80
|
+
switch (filter) {
|
|
81
|
+
case 0: break; // None
|
|
82
|
+
case 1: // Sub
|
|
83
|
+
for (let x = bpp; x < stride; x++) cur[x] = (cur[x] + cur[x - bpp]) & 0xFF;
|
|
84
|
+
break;
|
|
85
|
+
case 2: // Up
|
|
86
|
+
for (let x = 0; x < stride; x++) cur[x] = (cur[x] + prev[x]) & 0xFF;
|
|
87
|
+
break;
|
|
88
|
+
case 3: // Avg
|
|
89
|
+
for (let x = 0; x < stride; x++) {
|
|
90
|
+
const left = x >= bpp ? cur[x - bpp] : 0;
|
|
91
|
+
cur[x] = (cur[x] + ((left + prev[x]) >> 1)) & 0xFF;
|
|
92
|
+
}
|
|
93
|
+
break;
|
|
94
|
+
case 4: { // Paeth
|
|
95
|
+
for (let x = 0; x < stride; x++) {
|
|
96
|
+
const a = x >= bpp ? cur[x - bpp] : 0;
|
|
97
|
+
const b = prev[x];
|
|
98
|
+
const c = x >= bpp ? prev[x - bpp] : 0;
|
|
99
|
+
const p = a + b - c;
|
|
100
|
+
const pa = Math.abs(p - a);
|
|
101
|
+
const pb = Math.abs(p - b);
|
|
102
|
+
const pc = Math.abs(p - c);
|
|
103
|
+
const pred = pa <= pb && pa <= pc ? a : pb <= pc ? b : c;
|
|
104
|
+
cur[x] = (cur[x] + pred) & 0xFF;
|
|
105
|
+
}
|
|
106
|
+
break;
|
|
107
|
+
}
|
|
108
|
+
default: throw new Error(`unknown PNG filter ${filter}`);
|
|
109
|
+
}
|
|
110
|
+
// 16-bit samples are big-endian; we keep only the high byte (eyeball-
|
|
111
|
+
// identical to dividing by 257 for icon use).
|
|
112
|
+
for (let x = 0; x < width; x++) {
|
|
113
|
+
const di = (y * width + x) * 4;
|
|
114
|
+
const si = x * bpp;
|
|
115
|
+
if (bitDepth === 8) {
|
|
116
|
+
rgba[di + 0] = cur[si + 0];
|
|
117
|
+
rgba[di + 1] = cur[si + 1];
|
|
118
|
+
rgba[di + 2] = cur[si + 2];
|
|
119
|
+
rgba[di + 3] = samplesPerPixel === 4 ? cur[si + 3] : 255;
|
|
120
|
+
} else {
|
|
121
|
+
rgba[di + 0] = cur[si + 0]; // hi byte of R
|
|
122
|
+
rgba[di + 1] = cur[si + 2]; // hi byte of G
|
|
123
|
+
rgba[di + 2] = cur[si + 4]; // hi byte of B
|
|
124
|
+
rgba[di + 3] = samplesPerPixel === 4 ? cur[si + 6] : 255;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
const swap = prev; prev = cur; cur = swap;
|
|
128
|
+
}
|
|
129
|
+
return { width, height, rgba };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// ── Box-filter downsample (good enough for icon sizes) ───────────────────
|
|
133
|
+
function downsample(src, srcW, srcH, dstW, dstH) {
|
|
134
|
+
if (srcW === dstW && srcH === dstH) return Buffer.from(src);
|
|
135
|
+
const dst = Buffer.alloc(dstW * dstH * 4);
|
|
136
|
+
const sx = srcW / dstW;
|
|
137
|
+
const sy = srcH / dstH;
|
|
138
|
+
for (let y = 0; y < dstH; y++) {
|
|
139
|
+
const y0 = Math.floor(y * sy);
|
|
140
|
+
const y1 = Math.min(srcH, Math.ceil((y + 1) * sy));
|
|
141
|
+
for (let x = 0; x < dstW; x++) {
|
|
142
|
+
const x0 = Math.floor(x * sx);
|
|
143
|
+
const x1 = Math.min(srcW, Math.ceil((x + 1) * sx));
|
|
144
|
+
let r = 0, g = 0, b = 0, a = 0, n = 0;
|
|
145
|
+
for (let yy = y0; yy < y1; yy++) {
|
|
146
|
+
for (let xx = x0; xx < x1; xx++) {
|
|
147
|
+
const i = (yy * srcW + xx) * 4;
|
|
148
|
+
r += src[i]; g += src[i + 1]; b += src[i + 2]; a += src[i + 3];
|
|
149
|
+
n++;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
const di = (y * dstW + x) * 4;
|
|
153
|
+
dst[di] = (r / n) | 0;
|
|
154
|
+
dst[di + 1] = (g / n) | 0;
|
|
155
|
+
dst[di + 2] = (b / n) | 0;
|
|
156
|
+
dst[di + 3] = (a / n) | 0;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return dst;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// ── BMP DIB encoder for ICO entries (32bpp BGRA + AND mask) ──────────────
|
|
163
|
+
function encodeDIB(rgba, w, h) {
|
|
164
|
+
const colorBytes = w * h * 4;
|
|
165
|
+
const maskRowSize = ((w + 31) >> 5) << 2; // 1bpp, 4-byte aligned
|
|
166
|
+
const maskBytes = maskRowSize * h;
|
|
167
|
+
const out = Buffer.alloc(40 + colorBytes + maskBytes);
|
|
168
|
+
// BITMAPINFOHEADER
|
|
169
|
+
out.writeUInt32LE(40, 0); // biSize
|
|
170
|
+
out.writeInt32LE(w, 4); // biWidth
|
|
171
|
+
out.writeInt32LE(h * 2, 8); // biHeight = 2*h (color + mask)
|
|
172
|
+
out.writeUInt16LE(1, 12); // biPlanes
|
|
173
|
+
out.writeUInt16LE(32, 14); // biBitCount
|
|
174
|
+
out.writeUInt32LE(0, 16); // biCompression (BI_RGB)
|
|
175
|
+
// remaining BITMAPINFOHEADER fields stay zero
|
|
176
|
+
// Color: 32bpp BGRA, bottom-up
|
|
177
|
+
let off = 40;
|
|
178
|
+
for (let y = h - 1; y >= 0; y--) {
|
|
179
|
+
for (let x = 0; x < w; x++) {
|
|
180
|
+
const i = (y * w + x) * 4;
|
|
181
|
+
out[off++] = rgba[i + 2]; // B
|
|
182
|
+
out[off++] = rgba[i + 1]; // G
|
|
183
|
+
out[off++] = rgba[i + 0]; // R
|
|
184
|
+
out[off++] = rgba[i + 3]; // A
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
// AND mask stays all-zero — alpha channel handles transparency. Required
|
|
188
|
+
// by ICO format though, so we allocate the bytes (already zeroed).
|
|
189
|
+
return out;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// ── ICO container ────────────────────────────────────────────────────────
|
|
193
|
+
function buildICO(entries) {
|
|
194
|
+
const dirSize = 6 + 16 * entries.length;
|
|
195
|
+
const dir = Buffer.alloc(dirSize);
|
|
196
|
+
dir.writeUInt16LE(0, 0); // reserved
|
|
197
|
+
dir.writeUInt16LE(1, 2); // type = icon
|
|
198
|
+
dir.writeUInt16LE(entries.length, 4); // count
|
|
199
|
+
let dataOff = dirSize;
|
|
200
|
+
for (let i = 0; i < entries.length; i++) {
|
|
201
|
+
const e = entries[i];
|
|
202
|
+
const o = 6 + i * 16;
|
|
203
|
+
dir.writeUInt8(e.size === 256 ? 0 : e.size, o);
|
|
204
|
+
dir.writeUInt8(e.size === 256 ? 0 : e.size, o + 1);
|
|
205
|
+
dir.writeUInt8(0, o + 2); // ncolors
|
|
206
|
+
dir.writeUInt8(0, o + 3); // reserved
|
|
207
|
+
dir.writeUInt16LE(1, o + 4); // planes
|
|
208
|
+
dir.writeUInt16LE(32, o + 6); // bpp
|
|
209
|
+
dir.writeUInt32LE(e.data.length, o + 8);
|
|
210
|
+
dir.writeUInt32LE(dataOff, o + 12);
|
|
211
|
+
dataOff += e.data.length;
|
|
212
|
+
}
|
|
213
|
+
return Buffer.concat([dir, ...entries.map(e => e.data)]);
|
|
32
214
|
}
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
215
|
+
|
|
216
|
+
// ── Main ─────────────────────────────────────────────────────────────────
|
|
217
|
+
const decoded = decodePNG(pngBuf);
|
|
218
|
+
console.log(`build-icon-ico: source ${decoded.width}x${decoded.height} ${decoded.rgba.length} bytes RGBA`);
|
|
219
|
+
|
|
220
|
+
const sizes = [16, 32, 48, 64, 128, 256];
|
|
221
|
+
const entries = [];
|
|
222
|
+
for (const sz of sizes) {
|
|
223
|
+
const scaled = downsample(decoded.rgba, decoded.width, decoded.height, sz, sz);
|
|
224
|
+
if (sz === 256) {
|
|
225
|
+
// 256x256 BMP would be 256KB — store as PNG instead. Vista+ Shell
|
|
226
|
+
// accepts PNG entries at >=96px; <96px still has to be BMP for the
|
|
227
|
+
// taskbar code path on older configs.
|
|
228
|
+
const pngEntry = encodePNGEntry(scaled, sz);
|
|
229
|
+
entries.push({ size: sz, data: pngEntry });
|
|
230
|
+
} else {
|
|
231
|
+
entries.push({ size: sz, data: encodeDIB(scaled, sz, sz) });
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Re-encode an RGBA buffer back to a PNG (used only for the 256x256 entry).
|
|
236
|
+
function encodePNGEntry(rgba, sz) {
|
|
237
|
+
function chunk(type, data) {
|
|
238
|
+
const len = Buffer.alloc(4);
|
|
239
|
+
len.writeUInt32BE(data.length, 0);
|
|
240
|
+
const head = Buffer.concat([Buffer.from(type, "ascii"), data]);
|
|
241
|
+
const crc = Buffer.alloc(4);
|
|
242
|
+
crc.writeInt32BE(crc32(head), 0);
|
|
243
|
+
return Buffer.concat([len, head, crc]);
|
|
244
|
+
}
|
|
245
|
+
const sig = Buffer.from([0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]);
|
|
246
|
+
const ihdr = Buffer.alloc(13);
|
|
247
|
+
ihdr.writeUInt32BE(sz, 0);
|
|
248
|
+
ihdr.writeUInt32BE(sz, 4);
|
|
249
|
+
ihdr[8] = 8; // bit depth
|
|
250
|
+
ihdr[9] = 6; // RGBA
|
|
251
|
+
ihdr[10] = 0; // compression
|
|
252
|
+
ihdr[11] = 0; // filter
|
|
253
|
+
ihdr[12] = 0; // interlace
|
|
254
|
+
// Filter byte 0 (None) per scanline + RGBA pixels
|
|
255
|
+
const stride = sz * 4;
|
|
256
|
+
const filtered = Buffer.alloc(sz * (stride + 1));
|
|
257
|
+
for (let y = 0; y < sz; y++) {
|
|
258
|
+
filtered[y * (stride + 1)] = 0;
|
|
259
|
+
rgba.copy(filtered, y * (stride + 1) + 1, y * stride, y * stride + stride);
|
|
260
|
+
}
|
|
261
|
+
const idat = zlib.deflateSync(filtered, { level: 9 });
|
|
262
|
+
return Buffer.concat([sig, chunk("IHDR", ihdr), chunk("IDAT", idat), chunk("IEND", Buffer.alloc(0))]);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// Standard PNG-spec CRC32.
|
|
266
|
+
function crc32(buf) {
|
|
267
|
+
let c, table = crc32.table;
|
|
268
|
+
if (!table) {
|
|
269
|
+
table = crc32.table = new Int32Array(256);
|
|
270
|
+
for (let n = 0; n < 256; n++) {
|
|
271
|
+
c = n;
|
|
272
|
+
for (let k = 0; k < 8; k++) c = c & 1 ? (0xEDB88320 ^ (c >>> 1)) : (c >>> 1);
|
|
273
|
+
table[n] = c;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
c = -1;
|
|
277
|
+
for (let i = 0; i < buf.length; i++) c = table[(c ^ buf[i]) & 0xFF] ^ (c >>> 8);
|
|
278
|
+
return c ^ -1;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const ico = buildICO(entries);
|
|
58
282
|
fs.writeFileSync(dst, ico);
|
|
59
|
-
console.log(`build-icon-ico: wrote ${dst} (${ico.length} bytes, ${
|
|
283
|
+
console.log(`build-icon-ico: wrote ${dst} (${ico.length} bytes, sizes: ${sizes.join(", ")})`);
|
package/bin/mailx.js
CHANGED
|
@@ -83,6 +83,27 @@ function pidAlive(pid) {
|
|
|
83
83
|
return false;
|
|
84
84
|
}
|
|
85
85
|
}
|
|
86
|
+
/** True only if `pid` is alive AND its command line looks like a mailx node
|
|
87
|
+
* process. Windows reuses PIDs aggressively — a mailx that exited without
|
|
88
|
+
* running its cleanup handler can leave behind an instance.json whose PID
|
|
89
|
+
* has since been recycled by some other app (e.g. Creative Cloud UI Helper),
|
|
90
|
+
* making bare pidAlive() return true and wedging every subsequent `mailx`
|
|
91
|
+
* with "already running". Verify it's actually us. Non-Windows: fall back
|
|
92
|
+
* to pidAlive — POSIX PID reuse is rare enough that we don't bother. */
|
|
93
|
+
function pidIsMailx(pid) {
|
|
94
|
+
if (!pidAlive(pid))
|
|
95
|
+
return false;
|
|
96
|
+
if (process.platform !== "win32")
|
|
97
|
+
return true;
|
|
98
|
+
try {
|
|
99
|
+
const { execSync } = require("node:child_process");
|
|
100
|
+
const out = execSync(`powershell -NoProfile -Command "(Get-CimInstance Win32_Process -Filter \\"ProcessId=${pid}\\").CommandLine"`, { encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"], windowsHide: true }).toString();
|
|
101
|
+
return /mailx[\\\/](?:bin|packages|app)|mailx-server/i.test(out);
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
86
107
|
// Version-mismatch upgrade: if a daemon from an older version is running when
|
|
87
108
|
// the user types `mailx`, kill it so the new one can take over. Without this,
|
|
88
109
|
// a second invocation would silently no-op (daemon exists), leaving the user
|
|
@@ -93,7 +114,7 @@ const __commandFlags = ["kill", "v", "version", "setup", "add", "test", "rebuild
|
|
|
93
114
|
const __isCommandInvocation = process.argv.slice(2).some(a => __commandFlags.includes(a.replace(/^--?/, "")));
|
|
94
115
|
if (!isDaemon && !__isCommandInvocation) {
|
|
95
116
|
const inst = readInstanceFile();
|
|
96
|
-
if (inst &&
|
|
117
|
+
if (inst && pidIsMailx(inst.pid)) {
|
|
97
118
|
if (inst.version !== __selfVersion) {
|
|
98
119
|
console.log(`mailx: upgrading running daemon (PID ${inst.pid}) from v${inst.version} → v${__selfVersion}`);
|
|
99
120
|
try {
|
|
@@ -276,6 +297,12 @@ if (hasFlag("kill")) {
|
|
|
276
297
|
}
|
|
277
298
|
catch { /* */ }
|
|
278
299
|
}
|
|
300
|
+
// Always clear instance.json — the daemon's exit handler does this on a
|
|
301
|
+
// clean shutdown, but if it crashed (or was force-killed before the
|
|
302
|
+
// handler ran) the file lingers and wedges every future `mailx` with
|
|
303
|
+
// "already running" because Windows recycles PIDs aggressively. -kill
|
|
304
|
+
// is the user's escape hatch; leave no lock behind.
|
|
305
|
+
clearInstanceFile();
|
|
279
306
|
if (killed === 0)
|
|
280
307
|
console.log("No mailx processes found");
|
|
281
308
|
process.exit(0);
|