diff --git a/scripts/debug-tnef.ts b/scripts/debug-tnef.ts deleted file mode 100644 index c6144304..00000000 --- a/scripts/debug-tnef.ts +++ /dev/null @@ -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 '); - process.exit(1); -} - -const data = new Uint8Array(readFileSync(resolve(inputPath))); - -// TNEF attribute ID names -const ATTR_NAMES: Record = { - 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 = { - 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 = { - 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}`); diff --git a/scripts/generate-test-cert.ts b/scripts/generate-test-cert.ts deleted file mode 100644 index 0dad94bc..00000000 --- a/scripts/generate-test-cert.ts +++ /dev/null @@ -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[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); diff --git a/scripts/test-tnef.ts b/scripts/test-tnef.ts deleted file mode 100644 index 33173711..00000000 --- a/scripts/test-tnef.ts +++ /dev/null @@ -1,101 +0,0 @@ -/** - * Test script for TNEF (winmail.dat) parser. - * - * Usage: - * npx tsx scripts/test-tnef.ts - * - * 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 '); - 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 - ? `

Extracted Attachments (${result.attachments.length})

- - -${result.attachments.map((att, i) => ``).join('\n')} -
#NameMIME TypeSize
${i+1}${att.name}${att.mimeType}${att.data.byteLength} bytes
` - : '

No attachments found.

'; - -if (result.htmlBody) { - htmlContent = ` -TNEF Output - -

TNEF Parse Results

-

Source: ${inputPath} (${data.byteLength} bytes)

-${attachmentsList} -

HTML Body

-
-${result.htmlBody} -
-`; -} else if (result.body) { - const escaped = result.body - .replace(/&/g, '&') - .replace(//g, '>'); - htmlContent = ` -TNEF Output - -

TNEF Parse Results

-

Source: ${inputPath} (${data.byteLength} bytes)

-${attachmentsList} -

Plain Text Body

-
${escaped}
-`; -} else { - htmlContent = ` -TNEF Output - -

TNEF Parse Results

-

Source: ${inputPath} (${data.byteLength} bytes)

-

No body content found in this TNEF file. The email body is likely in the regular MIME text/plain part.

-${attachmentsList} -`; -} - -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.');