chore: remove scripts directory (moved to local-data)
This commit is contained in:
@@ -1,229 +0,0 @@
|
|||||||
/**
|
|
||||||
* Debug script for TNEF parser — dumps raw attribute structure.
|
|
||||||
*/
|
|
||||||
import { readFileSync } from 'fs';
|
|
||||||
import { resolve } from 'path';
|
|
||||||
|
|
||||||
const inputPath = process.argv[2];
|
|
||||||
if (!inputPath) {
|
|
||||||
console.error('Usage: npx tsx scripts/debug-tnef.ts <path-to-winmail.dat>');
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = new Uint8Array(readFileSync(resolve(inputPath)));
|
|
||||||
|
|
||||||
// TNEF attribute ID names
|
|
||||||
const ATTR_NAMES: Record<number, string> = {
|
|
||||||
0x00069003: 'attMAPIProps',
|
|
||||||
0x0002800C: 'attBody',
|
|
||||||
0x00069002: 'attAttachRenddata',
|
|
||||||
0x0006800F: 'attAttachData',
|
|
||||||
0x00018010: 'attAttachTitle',
|
|
||||||
0x00069005: 'attAttachment (MAPI)',
|
|
||||||
0x00028005: 'attSubject',
|
|
||||||
0x00068007: 'attMessageClass',
|
|
||||||
0x00078006: 'attDateSent',
|
|
||||||
0x00078008: 'attDateModified',
|
|
||||||
0x0006900B: 'attRecipTable',
|
|
||||||
0x00069001: 'attOwner',
|
|
||||||
0x00060001: 'attFrom',
|
|
||||||
0x00078004: 'attDateStart',
|
|
||||||
0x0001800A: 'attMessageID',
|
|
||||||
0x00050008: 'attPriority',
|
|
||||||
0x00040009: 'attAidOwner',
|
|
||||||
0x00010004: 'attConversationID',
|
|
||||||
0x0001800D: 'attParentID',
|
|
||||||
0x00018011: 'attAttachCreateDate',
|
|
||||||
0x00018012: 'attAttachModifyDate',
|
|
||||||
0x00060002: 'attDateRecd',
|
|
||||||
0x00060003: 'attAssignedTo',
|
|
||||||
};
|
|
||||||
|
|
||||||
const MAPI_PROP_NAMES: Record<number, string> = {
|
|
||||||
0x0037: 'PR_SUBJECT',
|
|
||||||
0x1000: 'PR_BODY',
|
|
||||||
0x1009: 'PR_RTF_COMPRESSED',
|
|
||||||
0x1013: 'PR_BODY_HTML',
|
|
||||||
0x1014: 'PR_BODY_CONTENT_ID',
|
|
||||||
0x0E1F: 'PR_RTF_IN_SYNC',
|
|
||||||
0x3701: 'PR_ATTACH_DATA_BIN',
|
|
||||||
0x3702: 'PR_ATTACH_ENCODING',
|
|
||||||
0x3703: 'PR_ATTACH_EXTENSION',
|
|
||||||
0x3704: 'PR_ATTACH_FILENAME',
|
|
||||||
0x3707: 'PR_ATTACH_LONG_FILENAME',
|
|
||||||
0x370E: 'PR_ATTACH_MIME_TAG',
|
|
||||||
0x3712: 'PR_ATTACH_CONTENT_ID',
|
|
||||||
0x0FF9: 'PR_RECORD_KEY',
|
|
||||||
0x0FFE: 'PR_OBJECT_TYPE',
|
|
||||||
0x3001: 'PR_DISPLAY_NAME',
|
|
||||||
0x3002: 'PR_ADDRTYPE',
|
|
||||||
0x3003: 'PR_EMAIL_ADDRESS',
|
|
||||||
};
|
|
||||||
|
|
||||||
const PROP_TYPE_NAMES: Record<number, string> = {
|
|
||||||
0x0002: 'PT_SHORT',
|
|
||||||
0x0003: 'PT_LONG',
|
|
||||||
0x000B: 'PT_BOOLEAN',
|
|
||||||
0x001E: 'PT_STRING8',
|
|
||||||
0x001F: 'PT_UNICODE',
|
|
||||||
0x0040: 'PT_SYSTIME',
|
|
||||||
0x0048: 'PT_CLSID',
|
|
||||||
0x0102: 'PT_BINARY',
|
|
||||||
0x0014: 'PT_I8',
|
|
||||||
};
|
|
||||||
|
|
||||||
function pad4(len: number): number {
|
|
||||||
return (4 - (len % 4)) % 4;
|
|
||||||
}
|
|
||||||
|
|
||||||
const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
|
|
||||||
let offset = 0;
|
|
||||||
|
|
||||||
function readU8() { return view.getUint8(offset++); }
|
|
||||||
function readU16() { const v = view.getUint16(offset, true); offset += 2; return v; }
|
|
||||||
function readU32() { const v = view.getUint32(offset, true); offset += 4; return v; }
|
|
||||||
function readBytes(n: number) { const s = data.slice(offset, offset + n); offset += n; return s; }
|
|
||||||
|
|
||||||
const sig = readU32();
|
|
||||||
console.log(`Signature: 0x${sig.toString(16)} (expected 0x223e9f78: ${sig === 0x223e9f78 ? 'OK' : 'MISMATCH'})`);
|
|
||||||
const key = readU16();
|
|
||||||
console.log(`Key: ${key}\n`);
|
|
||||||
|
|
||||||
let attrIndex = 0;
|
|
||||||
while (offset + 11 <= data.byteLength) {
|
|
||||||
const level = readU8();
|
|
||||||
const attrId = readU32();
|
|
||||||
const attrLen = readU32();
|
|
||||||
|
|
||||||
if (attrLen > data.byteLength - offset - 2) {
|
|
||||||
console.log(`[${attrIndex}] TRUNCATED — level=${level} id=0x${attrId.toString(16)} len=${attrLen} (remaining=${data.byteLength - offset})`);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
const attrData = readBytes(attrLen);
|
|
||||||
const checksum = readU16();
|
|
||||||
|
|
||||||
const levelStr = level === 1 ? 'MESSAGE' : level === 2 ? 'ATTACHMENT' : `LEVEL(${level})`;
|
|
||||||
const attrName = ATTR_NAMES[attrId] || `0x${attrId.toString(16).padStart(8, '0')}`;
|
|
||||||
|
|
||||||
console.log(`[${attrIndex}] ${levelStr} | ${attrName} | ${attrLen} bytes | checksum=0x${checksum.toString(16)}`);
|
|
||||||
|
|
||||||
// Dump MAPI props if this is a MAPI attr
|
|
||||||
if (attrId === 0x00069003 || attrId === 0x00069005) {
|
|
||||||
const propView = new DataView(attrData.buffer, attrData.byteOffset, attrData.byteLength);
|
|
||||||
let pOff = 0;
|
|
||||||
if (attrData.byteLength >= 4) {
|
|
||||||
const count = propView.getUint32(pOff, true); pOff += 4;
|
|
||||||
console.log(` MAPI props count: ${count}`);
|
|
||||||
|
|
||||||
for (let i = 0; i < count && pOff + 4 <= attrData.byteLength; i++) {
|
|
||||||
const propType = propView.getUint16(pOff, true); pOff += 2;
|
|
||||||
const propId = propView.getUint16(pOff, true); pOff += 2;
|
|
||||||
|
|
||||||
const baseType = propType & 0x0FFF;
|
|
||||||
const isMulti = (propType & 0x1000) !== 0;
|
|
||||||
const propName = MAPI_PROP_NAMES[propId] || `0x${propId.toString(16).padStart(4, '0')}`;
|
|
||||||
const typeName = PROP_TYPE_NAMES[baseType] || `0x${baseType.toString(16).padStart(4, '0')}`;
|
|
||||||
|
|
||||||
// Named props
|
|
||||||
if (propId >= 0x8000) {
|
|
||||||
if (pOff + 20 > attrData.byteLength) { console.log(` [${i}] ${propName} (${typeName}) — TRUNCATED (named prop)`); break; }
|
|
||||||
pOff += 16; // GUID
|
|
||||||
const kind = propView.getUint32(pOff, true); pOff += 4;
|
|
||||||
if (kind === 0) {
|
|
||||||
if (pOff + 4 > attrData.byteLength) break;
|
|
||||||
pOff += 4;
|
|
||||||
} else {
|
|
||||||
if (pOff + 4 > attrData.byteLength) break;
|
|
||||||
const nl = propView.getUint32(pOff, true); pOff += 4;
|
|
||||||
if (pOff + nl > attrData.byteLength) break;
|
|
||||||
pOff += nl + pad4(nl);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isMulti) {
|
|
||||||
if (pOff + 4 > attrData.byteLength) break;
|
|
||||||
const vc = propView.getUint32(pOff, true); pOff += 4;
|
|
||||||
console.log(` [${i}] ${propName} (${typeName} MV x${vc})`);
|
|
||||||
for (let j = 0; j < vc; j++) {
|
|
||||||
// skip values
|
|
||||||
if (baseType === 0x001E || baseType === 0x001F || baseType === 0x0102) {
|
|
||||||
if (pOff + 4 > attrData.byteLength) break;
|
|
||||||
const vl = propView.getUint32(pOff, true); pOff += 4;
|
|
||||||
pOff += vl + pad4(vl);
|
|
||||||
} else if (baseType === 0x0040 || baseType === 0x0014) {
|
|
||||||
pOff += 8;
|
|
||||||
} else if (baseType === 0x0048) {
|
|
||||||
pOff += 16;
|
|
||||||
} else if (baseType === 0x0002) {
|
|
||||||
pOff += 4;
|
|
||||||
} else {
|
|
||||||
pOff += 4;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
let valuePreview = '';
|
|
||||||
const savedOff = pOff;
|
|
||||||
|
|
||||||
if (baseType === 0x0002) {
|
|
||||||
if (pOff + 4 <= attrData.byteLength) {
|
|
||||||
valuePreview = `value=${propView.getUint16(pOff, true)}`;
|
|
||||||
pOff += 4; // padded
|
|
||||||
}
|
|
||||||
} else if (baseType === 0x0003 || baseType === 0x000B) {
|
|
||||||
if (pOff + 4 <= attrData.byteLength) {
|
|
||||||
valuePreview = `value=${propView.getUint32(pOff, true)}`;
|
|
||||||
pOff += 4;
|
|
||||||
}
|
|
||||||
} else if (baseType === 0x0014 || baseType === 0x0040) {
|
|
||||||
pOff += 8;
|
|
||||||
valuePreview = '(8 bytes)';
|
|
||||||
} else if (baseType === 0x0048) {
|
|
||||||
pOff += 16;
|
|
||||||
valuePreview = '(GUID)';
|
|
||||||
} else if (baseType === 0x001E || baseType === 0x001F || baseType === 0x0102) {
|
|
||||||
if (pOff + 4 <= attrData.byteLength) {
|
|
||||||
const vl = propView.getUint32(pOff, true); pOff += 4;
|
|
||||||
if (pOff + vl <= attrData.byteLength) {
|
|
||||||
const raw = attrData.slice(pOff, pOff + vl);
|
|
||||||
if (baseType === 0x001F) {
|
|
||||||
try { valuePreview = `"${new TextDecoder('utf-16le').decode(raw).slice(0, 120)}"`; } catch { valuePreview = `(${vl} bytes)`; }
|
|
||||||
} else if (baseType === 0x001E) {
|
|
||||||
try { valuePreview = `"${new TextDecoder('utf-8').decode(raw).slice(0, 120)}"`; } catch { valuePreview = `(${vl} bytes)`; }
|
|
||||||
} else {
|
|
||||||
valuePreview = `(${vl} bytes binary)`;
|
|
||||||
if (propId === 0x1013) {
|
|
||||||
try { valuePreview += ` preview="${new TextDecoder('utf-8').decode(raw).slice(0, 200)}"`; } catch { /* ignore decode errors */ }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pOff += vl + pad4(vl);
|
|
||||||
} else {
|
|
||||||
valuePreview = `(${vl} bytes — exceeds data)`;
|
|
||||||
pOff = savedOff + 4;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (pOff + 4 <= attrData.byteLength) {
|
|
||||||
pOff += 4;
|
|
||||||
valuePreview = '(4 bytes fixed)';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(` [${i}] ${propName} (${typeName}) ${valuePreview}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Preview plain text body/attach title
|
|
||||||
if (attrId === 0x0002800C || attrId === 0x00018010) {
|
|
||||||
try {
|
|
||||||
const preview = new TextDecoder('utf-8').decode(attrData.slice(0, Math.min(200, attrData.byteLength)));
|
|
||||||
console.log(` Preview: "${preview}"`);
|
|
||||||
} catch { /* ignore decode errors */ }
|
|
||||||
}
|
|
||||||
|
|
||||||
attrIndex++;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`\nTotal attributes: ${attrIndex}`);
|
|
||||||
@@ -1,160 +0,0 @@
|
|||||||
/**
|
|
||||||
* Generate a self-signed S/MIME test certificate (.p12) using pkijs.
|
|
||||||
* Usage: npx tsx scripts/generate-test-cert.ts
|
|
||||||
*/
|
|
||||||
import * as pkijs from 'pkijs';
|
|
||||||
import * as asn1js from 'asn1js';
|
|
||||||
import { writeFileSync } from 'fs';
|
|
||||||
import { join, dirname } from 'path';
|
|
||||||
import { fileURLToPath } from 'url';
|
|
||||||
|
|
||||||
const cryptoEngine = new pkijs.CryptoEngine({
|
|
||||||
crypto: crypto,
|
|
||||||
subtle: crypto.subtle,
|
|
||||||
name: 'webcrypto',
|
|
||||||
});
|
|
||||||
pkijs.setEngine('gen', crypto, cryptoEngine);
|
|
||||||
|
|
||||||
function stringToAB(str: string): ArrayBuffer {
|
|
||||||
const buf = new ArrayBuffer(str.length);
|
|
||||||
const view = new Uint8Array(buf);
|
|
||||||
for (let i = 0; i < str.length; i++) view[i] = str.charCodeAt(i);
|
|
||||||
return buf;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function main() {
|
|
||||||
const email = process.argv[2] || 'test@example.com';
|
|
||||||
const cn = email.split('@')[0];
|
|
||||||
const p12Password = 'test';
|
|
||||||
|
|
||||||
console.log(`Generating S/MIME certificate for ${email}...`);
|
|
||||||
|
|
||||||
// Generate RSA key pair for signing
|
|
||||||
const signKeyPair = await crypto.subtle.generateKey(
|
|
||||||
{ name: 'RSASSA-PKCS1-v1_5', modulusLength: 2048, publicExponent: new Uint8Array([1, 0, 1]), hash: 'SHA-256' },
|
|
||||||
true,
|
|
||||||
['sign', 'verify'],
|
|
||||||
);
|
|
||||||
|
|
||||||
// Build self-signed certificate
|
|
||||||
const cert = new pkijs.Certificate();
|
|
||||||
cert.version = 2;
|
|
||||||
cert.serialNumber = new asn1js.Integer({ value: Date.now() });
|
|
||||||
|
|
||||||
// Issuer = Subject (self-signed)
|
|
||||||
for (const name of [cert.issuer, cert.subject]) {
|
|
||||||
name.typesAndValues.push(
|
|
||||||
new pkijs.AttributeTypeAndValue({ type: '2.5.4.3', value: new asn1js.Utf8String({ value: cn }) }),
|
|
||||||
);
|
|
||||||
name.typesAndValues.push(
|
|
||||||
new pkijs.AttributeTypeAndValue({ type: '2.5.4.10', value: new asn1js.Utf8String({ value: 'Test Org' }) }),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
// Email in subject
|
|
||||||
cert.subject.typesAndValues.push(
|
|
||||||
new pkijs.AttributeTypeAndValue({ type: '1.2.840.113549.1.9.1', value: new asn1js.IA5String({ value: email }) }),
|
|
||||||
);
|
|
||||||
|
|
||||||
// Validity: 1 year
|
|
||||||
cert.notBefore.value = new Date();
|
|
||||||
const notAfter = new Date();
|
|
||||||
notAfter.setFullYear(notAfter.getFullYear() + 1);
|
|
||||||
cert.notAfter.value = notAfter;
|
|
||||||
|
|
||||||
// Import public key and sign
|
|
||||||
await cert.subjectPublicKeyInfo.importKey(signKeyPair.publicKey, cryptoEngine);
|
|
||||||
await cert.sign(signKeyPair.privateKey, 'SHA-256', cryptoEngine);
|
|
||||||
|
|
||||||
// Export private key as PKCS#8
|
|
||||||
const pkcs8Bytes = await crypto.subtle.exportKey('pkcs8', signKeyPair.privateKey);
|
|
||||||
|
|
||||||
// Build PKCS#12
|
|
||||||
const passwordBuf = stringToAB(p12Password);
|
|
||||||
|
|
||||||
const keyBag = new pkijs.PKCS8ShroudedKeyBag({
|
|
||||||
parsedValue: pkijs.PrivateKeyInfo.fromBER(pkcs8Bytes),
|
|
||||||
});
|
|
||||||
|
|
||||||
await keyBag.makeInternalValues({
|
|
||||||
password: passwordBuf,
|
|
||||||
contentEncryptionAlgorithm: {
|
|
||||||
name: 'AES-CBC',
|
|
||||||
length: 256,
|
|
||||||
} as Parameters<typeof keyBag.makeInternalValues>[0]['contentEncryptionAlgorithm'],
|
|
||||||
hmacHashAlgorithm: 'SHA-256',
|
|
||||||
iterationCount: 100_000,
|
|
||||||
});
|
|
||||||
|
|
||||||
const keyBagSafe = new pkijs.SafeBag({
|
|
||||||
bagId: '1.2.840.113549.1.12.10.1.2',
|
|
||||||
bagValue: keyBag,
|
|
||||||
bagAttributes: [
|
|
||||||
new pkijs.Attribute({
|
|
||||||
type: '1.2.840.113549.1.9.20', // friendlyName
|
|
||||||
values: [new asn1js.BmpString({ value: cn })],
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
||||||
const certBagSafe = new pkijs.SafeBag({
|
|
||||||
bagId: '1.2.840.113549.1.12.10.1.3',
|
|
||||||
bagValue: new pkijs.CertBag({ parsedValue: cert }),
|
|
||||||
bagAttributes: [
|
|
||||||
new pkijs.Attribute({
|
|
||||||
type: '1.2.840.113549.1.9.20',
|
|
||||||
values: [new asn1js.BmpString({ value: cn })],
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
||||||
const authenticatedSafe = new pkijs.AuthenticatedSafe({
|
|
||||||
parsedValue: {
|
|
||||||
safeContents: [
|
|
||||||
{ privacyMode: 0, value: new pkijs.SafeContents({ safeBags: [keyBagSafe] }) },
|
|
||||||
{ privacyMode: 0, value: new pkijs.SafeContents({ safeBags: [certBagSafe] }) },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
await authenticatedSafe.makeInternalValues({ safeContents: [{}, {}] });
|
|
||||||
|
|
||||||
const pfx = new pkijs.PFX({
|
|
||||||
parsedValue: {
|
|
||||||
integrityMode: 0,
|
|
||||||
authenticatedSafe,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
await pfx.makeInternalValues({
|
|
||||||
password: passwordBuf,
|
|
||||||
iterations: 100_000,
|
|
||||||
pbkdf2HashAlgorithm: 'SHA-256',
|
|
||||||
hmacHashAlgorithm: 'SHA-256',
|
|
||||||
});
|
|
||||||
|
|
||||||
const p12Bytes = pfx.toSchema().toBER(false);
|
|
||||||
|
|
||||||
// Also export the public cert as PEM
|
|
||||||
const certDer = cert.toSchema(true).toBER(false);
|
|
||||||
const certB64 = Buffer.from(certDer).toString('base64');
|
|
||||||
const certPem = `-----BEGIN CERTIFICATE-----\n${certB64.match(/.{1,64}/g)!.join('\n')}\n-----END CERTIFICATE-----\n`;
|
|
||||||
|
|
||||||
const slug = email.replace(/[@.]/g, '-');
|
|
||||||
const outDir = join(dirname(fileURLToPath(import.meta.url)), '..', 'local-data');
|
|
||||||
const p12Path = join(outDir, `${slug}.p12`);
|
|
||||||
const pemPath = join(outDir, `${slug}-cert.pem`);
|
|
||||||
|
|
||||||
writeFileSync(p12Path, Buffer.from(p12Bytes));
|
|
||||||
writeFileSync(pemPath, certPem);
|
|
||||||
|
|
||||||
console.log(`\nFiles written:`);
|
|
||||||
console.log(` ${p12Path}`);
|
|
||||||
console.log(` ${pemPath}`);
|
|
||||||
console.log(`\nCredentials:`);
|
|
||||||
console.log(` Email: ${email}`);
|
|
||||||
console.log(` CN: ${cn}`);
|
|
||||||
console.log(` Password: ${p12Password}`);
|
|
||||||
console.log(` Valid until: ${notAfter.toISOString().split('T')[0]}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
main().catch(console.error);
|
|
||||||
@@ -1,101 +0,0 @@
|
|||||||
/**
|
|
||||||
* Test script for TNEF (winmail.dat) parser.
|
|
||||||
*
|
|
||||||
* Usage:
|
|
||||||
* npx tsx scripts/test-tnef.ts <path-to-winmail.dat>
|
|
||||||
*
|
|
||||||
* Outputs:
|
|
||||||
* - tnef-output.html (HTML body or formatted plain text)
|
|
||||||
* - Any extracted attachments saved alongside
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { readFileSync, writeFileSync } from 'fs';
|
|
||||||
import { resolve, basename } from 'path';
|
|
||||||
import { parseTnef } from '../lib/tnef';
|
|
||||||
|
|
||||||
const inputPath = process.argv[2];
|
|
||||||
if (!inputPath) {
|
|
||||||
console.error('Usage: npx tsx scripts/test-tnef.ts <path-to-winmail.dat>');
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
const fullPath = resolve(inputPath);
|
|
||||||
console.log(`Reading: ${fullPath}`);
|
|
||||||
|
|
||||||
const data = new Uint8Array(readFileSync(fullPath));
|
|
||||||
console.log(`File size: ${data.byteLength} bytes`);
|
|
||||||
|
|
||||||
const result = parseTnef(data);
|
|
||||||
|
|
||||||
console.log(`\n=== TNEF Parse Results ===`);
|
|
||||||
console.log(`Plain text body: ${result.body ? `${result.body.length} chars` : 'none'}`);
|
|
||||||
console.log(`HTML body: ${result.htmlBody ? `${result.htmlBody.length} chars` : 'none'}`);
|
|
||||||
console.log(`Attachments: ${result.attachments.length}`);
|
|
||||||
|
|
||||||
if (result.attachments.length > 0) {
|
|
||||||
console.log(`\nAttachments:`);
|
|
||||||
result.attachments.forEach((att, i) => {
|
|
||||||
console.log(` [${i + 1}] ${att.name} (${att.mimeType}, ${att.data.byteLength} bytes)`);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build output HTML
|
|
||||||
let htmlContent: string;
|
|
||||||
|
|
||||||
const attachmentsList = result.attachments.length > 0
|
|
||||||
? `<h3>Extracted Attachments (${result.attachments.length})</h3>
|
|
||||||
<table border="1" cellpadding="8" cellspacing="0" style="border-collapse:collapse;font-family:sans-serif;">
|
|
||||||
<tr style="background:#f0f0f0;"><th>#</th><th>Name</th><th>MIME Type</th><th>Size</th></tr>
|
|
||||||
${result.attachments.map((att, i) => `<tr><td>${i+1}</td><td>${att.name}</td><td>${att.mimeType}</td><td>${att.data.byteLength} bytes</td></tr>`).join('\n')}
|
|
||||||
</table>`
|
|
||||||
: '<p>No attachments found.</p>';
|
|
||||||
|
|
||||||
if (result.htmlBody) {
|
|
||||||
htmlContent = `<!DOCTYPE html>
|
|
||||||
<html><head><meta charset="utf-8"><title>TNEF Output</title></head>
|
|
||||||
<body style="font-family:sans-serif;max-width:900px;margin:20px auto;">
|
|
||||||
<h2 style="color:#333;border-bottom:2px solid #0078d4;padding-bottom:8px;">TNEF Parse Results</h2>
|
|
||||||
<p><strong>Source:</strong> ${inputPath} (${data.byteLength} bytes)</p>
|
|
||||||
${attachmentsList}
|
|
||||||
<h3>HTML Body</h3>
|
|
||||||
<div style="border:1px solid #ccc;padding:16px;border-radius:4px;background:#fff;">
|
|
||||||
${result.htmlBody}
|
|
||||||
</div>
|
|
||||||
</body></html>`;
|
|
||||||
} else if (result.body) {
|
|
||||||
const escaped = result.body
|
|
||||||
.replace(/&/g, '&')
|
|
||||||
.replace(/</g, '<')
|
|
||||||
.replace(/>/g, '>');
|
|
||||||
htmlContent = `<!DOCTYPE html>
|
|
||||||
<html><head><meta charset="utf-8"><title>TNEF Output</title></head>
|
|
||||||
<body style="font-family:sans-serif;max-width:900px;margin:20px auto;">
|
|
||||||
<h2 style="color:#333;border-bottom:2px solid #0078d4;padding-bottom:8px;">TNEF Parse Results</h2>
|
|
||||||
<p><strong>Source:</strong> ${inputPath} (${data.byteLength} bytes)</p>
|
|
||||||
${attachmentsList}
|
|
||||||
<h3>Plain Text Body</h3>
|
|
||||||
<pre style="font-family:Consolas,monospace;white-space:pre-wrap;line-height:1.6;border:1px solid #ccc;padding:16px;border-radius:4px;background:#fff;">${escaped}</pre>
|
|
||||||
</body></html>`;
|
|
||||||
} else {
|
|
||||||
htmlContent = `<!DOCTYPE html>
|
|
||||||
<html><head><meta charset="utf-8"><title>TNEF Output</title></head>
|
|
||||||
<body style="font-family:sans-serif;max-width:900px;margin:20px auto;">
|
|
||||||
<h2 style="color:#333;border-bottom:2px solid #0078d4;padding-bottom:8px;">TNEF Parse Results</h2>
|
|
||||||
<p><strong>Source:</strong> ${inputPath} (${data.byteLength} bytes)</p>
|
|
||||||
<p style="color:#666;"><em>No body content found in this TNEF file. The email body is likely in the regular MIME text/plain part.</em></p>
|
|
||||||
${attachmentsList}
|
|
||||||
</body></html>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const outputHtml = resolve('tnef-output.html');
|
|
||||||
writeFileSync(outputHtml, htmlContent, 'utf-8');
|
|
||||||
console.log(`\nSaved HTML: ${outputHtml}`);
|
|
||||||
|
|
||||||
// Save extracted attachments
|
|
||||||
result.attachments.forEach((att, i) => {
|
|
||||||
const attPath = resolve(`tnef-attachment-${i + 1}-${att.name}`);
|
|
||||||
writeFileSync(attPath, att.data);
|
|
||||||
console.log(`Saved attachment: ${attPath}`);
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log('\nDone.');
|
|
||||||
Reference in New Issue
Block a user