From f720cb3ef898ccff01e536f62a1752c9622b758e Mon Sep 17 00:00:00 2001 From: Linus Rath <139418639+rathlinus@users.noreply.github.com> Date: Tue, 17 Mar 2026 21:22:43 +0100 Subject: [PATCH] feat: add TNEF (winmail.dat) support for email attachments and parsing --- components/email/email-viewer.tsx | 137 ++++- components/email/thread-conversation-view.tsx | 8 +- lib/tnef.ts | 347 +++++++++++ scripts/debug-tnef.ts | 229 +++++++ scripts/test-tnef.ts | 101 +++ tnef-attachment-1-zappa_av1.jpg | Bin 0 -> 2937 bytes tnef-attachment-2-bookmark.htm | 578 ++++++++++++++++++ tnef-output.html | 52 ++ 8 files changed, 1432 insertions(+), 20 deletions(-) create mode 100644 lib/tnef.ts create mode 100644 scripts/debug-tnef.ts create mode 100644 scripts/test-tnef.ts create mode 100644 tnef-attachment-1-zappa_av1.jpg create mode 100644 tnef-attachment-2-bookmark.htm create mode 100644 tnef-output.html diff --git a/components/email/email-viewer.tsx b/components/email/email-viewer.tsx index b670c9db..ee0b3b0e 100644 --- a/components/email/email-viewer.tsx +++ b/components/email/email-viewer.tsx @@ -86,6 +86,8 @@ import { smimeDecrypt, SmimeKeyLockedError, normalizeCmsBytes } from "@/lib/smim import { smimeVerify } from "@/lib/smime/smime-verify"; import { useSmimeStore } from "@/stores/smime-store"; import type { SmimeStatus } from "@/lib/smime/types"; +import { parseTnef, isTnefAttachment } from "@/lib/tnef"; +import type { TnefAttachment } from "@/lib/tnef"; interface EmailViewerProps { email: Email | null; @@ -493,6 +495,7 @@ interface EffectiveAttachment { blobId?: string; cid?: string; decryptedAttachment?: PostalMimeAttachment; + tnefData?: Uint8Array; } function getPostalMimeAttachmentSize(attachment: PostalMimeAttachment): number { @@ -859,6 +862,11 @@ export function EmailViewer({ const [smimeUnlockError, setSmimeUnlockError] = useState(null); const smimeStore = useSmimeStore(); + // TNEF (winmail.dat) support + const [tnefHtml, setTnefHtml] = useState(null); + const [tnefText, setTnefText] = useState(null); + const [tnefAttachments, setTnefAttachments] = useState([]); + // Ensure S/MIME key records are loaded from IndexedDB useEffect(() => { smimeStore.load(); @@ -1021,6 +1029,9 @@ export function EmailViewer({ setSmimeUnlockDialogOpen(false); setSmimeUnlockTargetId(null); setSmimeUnlockError(null); + setTnefHtml(null); + setTnefText(null); + setTnefAttachments([]); }, [email?.id, externalContentPolicy]); const prepareSmimeUnlock = useCallback((keyRecordId: string) => { @@ -1598,6 +1609,51 @@ export function EmailViewer({ smimeStore.unlockedDecryptionKeys, ]); + // TNEF (winmail.dat) detection and processing + useEffect(() => { + if (!email?.attachments || !client) return; + + const tnefAtt = email.attachments.find(att => isTnefAttachment(att.name, att.type)); + if (!tnefAtt?.blobId) return; + + // Only process if the email has no usable HTML body + const hasHtmlBody = !!( + email.htmlBody?.[0]?.partId && + email.bodyValues?.[email.htmlBody[0].partId]?.value?.trim() + ); + if (hasHtmlBody) return; + + let cancelled = false; + + async function processTnef() { + try { + const blobBytes = await client!.fetchBlobArrayBuffer(tnefAtt!.blobId!); + if (cancelled || blobBytes.byteLength === 0) return; + + const tnefData = new Uint8Array(blobBytes); + const parsed = parseTnef(tnefData); + + if (cancelled) return; + + if (parsed.htmlBody) { + setTnefHtml(parsed.htmlBody); + } + if (parsed.body) { + setTnefText(parsed.body); + } + if (parsed.attachments.length > 0) { + setTnefAttachments(parsed.attachments); + } + } catch { + // TNEF parsing failed — fall through to plain text display + } + } + + processTnef(); + + return () => { cancelled = true; }; + }, [email, client]); + // Fetch inline CID images with authentication to prevent browser auth dialogs useEffect(() => { let cancelled = false; @@ -1678,15 +1734,29 @@ export function EmailViewer({ })); } - return (email?.attachments ?? []).map((attachment, index) => ({ - id: attachment.blobId || `${attachment.name || 'attachment'}-${index}`, - name: attachment.name || null, - type: attachment.type || 'application/octet-stream', - size: attachment.size, - blobId: attachment.blobId, - cid: attachment.cid, + const jmapAttachments = (email?.attachments ?? []) + // Hide winmail.dat when we have successfully extracted TNEF content + .filter(att => !(tnefHtml || tnefText) || !isTnefAttachment(att.name, att.type)) + .map((attachment, index) => ({ + id: attachment.blobId || `${attachment.name || 'attachment'}-${index}`, + name: attachment.name || null, + type: attachment.type || 'application/octet-stream', + size: attachment.size, + blobId: attachment.blobId, + cid: attachment.cid, + })); + + // Append attachments extracted from TNEF + const tnefExtracted: EffectiveAttachment[] = tnefAttachments.map((att, index) => ({ + id: `tnef-${index}-${att.name}`, + name: att.name, + type: att.mimeType, + size: att.data.byteLength, + tnefData: att.data, })); - }, [email?.attachments, smimeDecryptedAttachments]); + + return [...jmapAttachments, ...tnefExtracted]; + }, [email?.attachments, smimeDecryptedAttachments, tnefHtml, tnefText, tnefAttachments]); // Generate email source for viewing const generateEmailSource = (email: Email): string => { @@ -1952,14 +2022,11 @@ export function EmailViewer({ const textContent = email.bodyValues[email.textBody[0].partId].value; // Convert plain text to HTML with proper formatting + // Uses white-space: pre-wrap on the container to preserve newlines/whitespace const htmlFromText = textContent .replace(/&/g, '&') .replace(//g, '>') - .replace(/\r\n/g, '
') // Windows line endings - .replace(/\r/g, '
') // Old Mac line endings - .replace(/\n/g, '
') // Unix line endings - .replace(/\t/g, '    ') // Convert tabs to spaces .replace(/(https?:\/\/[^\s<]+)/g, '$1'); return { @@ -1974,10 +2041,7 @@ export function EmailViewer({ const previewHtml = email.preview .replace(/&/g, '&') .replace(//g, '>') - .replace(/\r\n/g, '
') - .replace(/\r/g, '
') - .replace(/\n/g, '
'); + .replace(/>/g, '>'); return { html: `
${previewHtml}
`, @@ -2008,12 +2072,24 @@ export function EmailViewer({ .replace(/&/g, '&') .replace(//g, '>') - .replace(/\n/g, '
') + .replace(/(https?:\/\/[^\s<]+)/g, '$1'); + return { html: htmlFromText, isHtml: false }; + } + // TNEF (winmail.dat) extracted content + if (tnefHtml) { + const cleanHtml = DOMPurify.sanitize(tnefHtml, EMAIL_SANITIZE_CONFIG); + return { html: cleanHtml, isHtml: true }; + } + if (tnefText) { + const htmlFromText = tnefText + .replace(/&/g, '&') + .replace(//g, '>') .replace(/(https?:\/\/[^\s<]+)/g, '$1'); return { html: htmlFromText, isHtml: false }; } return emailContent; - }, [cidBlobUrls, emailContent, smimeDecryptedHtml, smimeDecryptedText]); + }, [cidBlobUrls, emailContent, smimeDecryptedHtml, smimeDecryptedText, tnefHtml, tnefText]); const handleEffectiveAttachmentOpen = useCallback((attachment: EffectiveAttachment) => { const isPreviewable = isFilePreviewable(attachment.name || undefined, attachment.type); @@ -2024,6 +2100,30 @@ export function EmailViewer({ return; } + // Handle TNEF-extracted attachments + if (attachment.tnefData) { + const buffer = attachment.tnefData.buffer.slice( + attachment.tnefData.byteOffset, + attachment.tnefData.byteOffset + attachment.tnefData.byteLength, + ) as ArrayBuffer; + const blob = new Blob([buffer], { type: attachment.type || 'application/octet-stream' }); + const objectUrl = URL.createObjectURL(blob); + + if (opensPreview) { + window.open(objectUrl, '_blank', 'noopener,noreferrer'); + } else { + const anchor = document.createElement('a'); + anchor.href = objectUrl; + anchor.download = attachment.name || 'download'; + document.body.appendChild(anchor); + anchor.click(); + anchor.remove(); + } + + setTimeout(() => URL.revokeObjectURL(objectUrl), 60000); + return; + } + if (!attachment.decryptedAttachment) { return; } @@ -4021,6 +4121,7 @@ export function EmailViewer({ fontSize: '14px', lineHeight: '1.6', wordBreak: 'break-word', + whiteSpace: 'pre-wrap', }} /> )} diff --git a/components/email/thread-conversation-view.tsx b/components/email/thread-conversation-view.tsx index c35f72c7..572bd793 100644 --- a/components/email/thread-conversation-view.tsx +++ b/components/email/thread-conversation-view.tsx @@ -404,7 +404,6 @@ function EmailCard({ .replace(/&/g, '&') .replace(//g, '>') - .replace(/\n/g, '
') .replace(/(https?:\/\/[^\s<]+)/g, '$1'); return { html: htmlEscaped, isHtml: false }; } @@ -412,7 +411,11 @@ function EmailCard({ // Fallback to preview if (email.preview) { - return { html: email.preview.replace(/\n/g, '
'), isHtml: false }; + const previewHtml = email.preview + .replace(/&/g, '&') + .replace(//g, '>'); + return { html: previewHtml, isHtml: false }; } return { html: "", isHtml: false }; @@ -521,6 +524,7 @@ function EmailCard({ "[&_table]:border-collapse [&_td]:p-2 [&_th]:p-2", "[&_img]:max-w-full [&_img]:h-auto" )} + style={!emailContent.isHtml ? { whiteSpace: 'pre-wrap', fontFamily: 'ui-monospace, "SF Mono", Consolas, monospace', fontSize: '13px' } : undefined} dangerouslySetInnerHTML={{ __html: emailContent.html }} /> diff --git a/lib/tnef.ts b/lib/tnef.ts new file mode 100644 index 00000000..e6a88a75 --- /dev/null +++ b/lib/tnef.ts @@ -0,0 +1,347 @@ +/** + * Minimal TNEF (Transport Neutral Encapsulation Format) parser. + * + * Parses winmail.dat files sent by Microsoft Outlook to extract + * the HTML body, plain text body, and embedded attachments. + * + * Reference: MS-OXTNEF / MS-TNEF specification. + */ + +// TNEF signature +const TNEF_SIGNATURE = 0x223E9F78; + +// Attribute levels +const LVL_MESSAGE = 0x01; +const LVL_ATTACHMENT = 0x02; + +// Message-level attribute IDs +const attBody = 0x0002800C; +const attMAPIProps = 0x00069003; + +// Attachment-level attribute IDs +const attAttachRenddata = 0x00069002; +const attAttachData = 0x0006800F; +const attAttachTitle = 0x00018010; +const attAttachment = 0x00069005; // MAPI props for attachments + +// MAPI property types +const PT_SHORT = 0x0002; +const PT_LONG = 0x0003; +const PT_BOOLEAN = 0x000B; +const PT_STRING8 = 0x001E; +const PT_UNICODE = 0x001F; +const PT_BINARY = 0x0102; +const PT_SYSTIME = 0x0040; +const PT_CLSID = 0x0048; +const PT_I8 = 0x0014; + +// Multi-value flag +const MV_FLAG = 0x1000; + +// MAPI property IDs +const PR_BODY = 0x1000; +const PR_BODY_HTML = 0x1013; +const PR_ATTACH_LONG_FILENAME = 0x3707; +const PR_ATTACH_MIME_TAG = 0x370E; +const PR_ATTACH_DATA_BIN = 0x3701; + +export interface TnefAttachment { + name: string; + mimeType: string; + data: Uint8Array; +} + +export interface TnefResult { + body: string | null; + htmlBody: string | null; + attachments: TnefAttachment[]; +} + +class BinaryReader { + private view: DataView; + private offset: number; + private bytes: Uint8Array; + + constructor(data: Uint8Array) { + this.bytes = data; + this.view = new DataView(data.buffer, data.byteOffset, data.byteLength); + this.offset = 0; + } + + readUint8(): number { + const val = this.view.getUint8(this.offset); + this.offset += 1; + return val; + } + + readUint16LE(): number { + const val = this.view.getUint16(this.offset, true); + this.offset += 2; + return val; + } + + readUint32LE(): number { + const val = this.view.getUint32(this.offset, true); + this.offset += 4; + return val; + } + + readBytes(length: number): Uint8Array { + const slice = this.bytes.slice(this.offset, this.offset + length); + this.offset += length; + return slice; + } + + skip(n: number): void { + this.offset += n; + } + + get remaining(): number { + return this.bytes.byteLength - this.offset; + } +} + +/** Padding needed to align to 4-byte boundary */ +function pad4(len: number): number { + return (4 - (len % 4)) % 4; +} + +/** Read a single MAPI property value (fixed-length types only) */ +function readMAPIFixedValue(r: BinaryReader, propType: number): Uint8Array | number | null { + switch (propType) { + case PT_SHORT: { + const val = r.readUint16LE(); + r.skip(2); // padded to 4 bytes + return val; + } + case PT_LONG: + case PT_BOOLEAN: + return r.readUint32LE(); + case PT_I8: + case PT_SYSTIME: + return r.readBytes(8); + case PT_CLSID: + return r.readBytes(16); + default: + // Unknown/unsupported type — try to read as fixed 4 bytes + if (r.remaining >= 4) { + return r.readBytes(4); + } + return null; + } +} + +/** Read a variable-length MAPI value (length-prefixed with padding) */ +function readMAPIVarValue(r: BinaryReader): Uint8Array | null { + if (r.remaining < 4) return null; + const length = r.readUint32LE(); + if (length > r.remaining) return null; + const data = r.readBytes(length); + r.skip(pad4(length)); + return data; +} + +/** Check if a base property type is variable-length */ +function isVarLengthType(baseType: number): boolean { + return baseType === PT_STRING8 || baseType === PT_UNICODE || baseType === PT_BINARY; +} + +/** Decode a MAPI string (PT_STRING8 or PT_UNICODE) from raw bytes */ +function decodeMAPIString(data: Uint8Array, propType: number): string { + if (propType === PT_UNICODE) { + let len = data.byteLength; + // Strip null terminator (2 bytes for UTF-16) + if (len >= 2 && data[len - 1] === 0 && data[len - 2] === 0) { + len -= 2; + } + return new TextDecoder('utf-16le').decode(data.subarray(0, len)); + } + let len = data.byteLength; + if (len >= 1 && data[len - 1] === 0) { + len -= 1; + } + return new TextDecoder('utf-8').decode(data.subarray(0, len)); +} + +/** Parse MAPI properties from a raw attribute data block */ +function parseMAPIProps(data: Uint8Array): Map { + const props = new Map(); + const r = new BinaryReader(data); + + if (r.remaining < 4) return props; + const count = r.readUint32LE(); + + for (let i = 0; i < count && r.remaining >= 4; i++) { + const propType = r.readUint16LE(); + const propID = r.readUint16LE(); + + // Named properties (ID >= 0x8000) carry extra GUID + name data + if (propID >= 0x8000) { + if (r.remaining < 20) break; + r.skip(16); // GUID + const kind = r.readUint32LE(); + if (kind === 0) { + if (r.remaining < 4) break; + r.skip(4); // named-by-ID + } else { + if (r.remaining < 4) break; + const nameLen = r.readUint32LE(); + if (nameLen > r.remaining) break; + r.skip(nameLen); + r.skip(pad4(nameLen)); + } + } + + const baseType = propType & 0x0FFF; + const isMultiValue = (propType & MV_FLAG) !== 0; + + if (isVarLengthType(baseType)) { + // Variable-length types always have a value count (1 for single-value) + if (r.remaining < 4) break; + const valueCount = r.readUint32LE(); + let lastValue: Uint8Array | null = null; + for (let j = 0; j < valueCount && r.remaining > 0; j++) { + lastValue = readMAPIVarValue(r); + } + if (!isMultiValue && lastValue) { + props.set(propID, { type: propType, value: lastValue }); + } + } else if (isMultiValue) { + if (r.remaining < 4) break; + const valueCount = r.readUint32LE(); + for (let j = 0; j < valueCount && r.remaining > 0; j++) { + readMAPIFixedValue(r, baseType); + } + } else { + const value = readMAPIFixedValue(r, baseType); + props.set(propID, { type: propType, value }); + } + } + + return props; +} + +/** + * Parse a TNEF (winmail.dat) file and extract the body and attachments. + * + * @param data - Raw bytes of the TNEF file + * @returns Parsed result with body text, HTML body, and attachments + */ +export function parseTnef(data: Uint8Array): TnefResult { + const result: TnefResult = { + body: null, + htmlBody: null, + attachments: [], + }; + + if (data.byteLength < 6) return result; + + const r = new BinaryReader(data); + + const signature = r.readUint32LE(); + if (signature !== TNEF_SIGNATURE) { + return result; + } + + r.skip(2); // legacy key + + // Current attachment being assembled + let curAttach: { name: string; mimeType: string; data: Uint8Array | null } | null = null; + + while (r.remaining >= 11) { + const level = r.readUint8(); + const attrID = r.readUint32LE(); + const attrLen = r.readUint32LE(); + + if (attrLen > r.remaining - 2) break; // not enough data for payload + checksum + + const attrData = r.readBytes(attrLen); + r.skip(2); // checksum + + if (level === LVL_MESSAGE) { + if (attrID === attBody) { + result.body = new TextDecoder('utf-8').decode(attrData); + } else if (attrID === attMAPIProps) { + const props = parseMAPIProps(attrData); + + // HTML body + const htmlProp = props.get(PR_BODY_HTML); + if (htmlProp?.value instanceof Uint8Array) { + const baseType = htmlProp.type & 0x0FFF; + if (baseType === PT_STRING8 || baseType === PT_UNICODE) { + result.htmlBody = decodeMAPIString(htmlProp.value, baseType); + } else { + result.htmlBody = new TextDecoder('utf-8').decode(htmlProp.value); + } + } + + // Plain text body from MAPI props (fallback) + if (!result.body) { + const bodyProp = props.get(PR_BODY); + if (bodyProp?.value instanceof Uint8Array) { + result.body = decodeMAPIString(bodyProp.value, bodyProp.type & 0x0FFF); + } + } + } + } else if (level === LVL_ATTACHMENT) { + if (attrID === attAttachRenddata) { + // Start of a new attachment — flush previous + if (curAttach?.data) { + result.attachments.push({ + name: curAttach.name, + mimeType: curAttach.mimeType, + data: curAttach.data, + }); + } + curAttach = { name: 'attachment', mimeType: 'application/octet-stream', data: null }; + } else if (attrID === attAttachTitle && curAttach) { + let len = attrData.byteLength; + if (len > 0 && attrData[len - 1] === 0) len--; + curAttach.name = new TextDecoder('utf-8').decode(attrData.subarray(0, len)); + } else if (attrID === attAttachData && curAttach) { + curAttach.data = attrData; + } else if (attrID === attAttachment && curAttach) { + const props = parseMAPIProps(attrData); + + const longName = props.get(PR_ATTACH_LONG_FILENAME); + if (longName?.value instanceof Uint8Array) { + curAttach.name = decodeMAPIString(longName.value, longName.type & 0x0FFF); + } + + const mimeTag = props.get(PR_ATTACH_MIME_TAG); + if (mimeTag?.value instanceof Uint8Array) { + curAttach.mimeType = decodeMAPIString(mimeTag.value, mimeTag.type & 0x0FFF); + } + + const attachData = props.get(PR_ATTACH_DATA_BIN); + if (attachData?.value instanceof Uint8Array) { + curAttach.data = attachData.value; + } + } + } + } + + // Flush last attachment + if (curAttach?.data) { + result.attachments.push({ + name: curAttach.name, + mimeType: curAttach.mimeType, + data: curAttach.data, + }); + } + + return result; +} + +/** + * Check if a MIME attachment is a TNEF (winmail.dat) file. + */ +export function isTnefAttachment(name?: string | null, type?: string): boolean { + const lowerName = (name || '').toLowerCase(); + const lowerType = (type || '').toLowerCase(); + return ( + lowerName === 'winmail.dat' || + lowerType === 'application/ms-tnef' || + lowerType === 'application/vnd.ms-tnef' + ); +} diff --git a/scripts/debug-tnef.ts b/scripts/debug-tnef.ts new file mode 100644 index 00000000..edbb7933 --- /dev/null +++ b/scripts/debug-tnef.ts @@ -0,0 +1,229 @@ +/** + * 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 {} + } + } + 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 {} + } + + attrIndex++; +} + +console.log(`\nTotal attributes: ${attrIndex}`); diff --git a/scripts/test-tnef.ts b/scripts/test-tnef.ts new file mode 100644 index 00000000..33173711 --- /dev/null +++ b/scripts/test-tnef.ts @@ -0,0 +1,101 @@ +/** + * 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.'); diff --git a/tnef-attachment-1-zappa_av1.jpg b/tnef-attachment-1-zappa_av1.jpg new file mode 100644 index 0000000000000000000000000000000000000000..da46742cd64aef3da4865dcda1892826ae1bbc49 GIT binary patch literal 2937 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 + +Bookmarks +

Bookmarks

+

+

Adobe GoLive

+

+

GoLive Actions Resource +
GoLive Actions +
GoLive Headquarters News +
GoLiveHeaven A Hub for the GoLive Community +
http--216.246.51.202-forums-adobe_golive-index.htm +
More Adobe GoLive Actions page one +
OUTactions +

+

ASP Stuff

+

+

ASP - Application service providers +
CRMXchange Gateway to the Contact Center, CRM and Customer Care Community +
PeopleSoft Customer Relationship Management Home +

+

Daily

+

+

Astronomy Picture of the Day Archive +
Best of the Blogs +
Brill's Content +
CamWorld Thinking Outside the Box +
CHUD - Cinematic Happenings Under Development +
Daypop - a current events-weblog-news search engine +
Dotcom Scoop +
dotCULT.com +
drew##^# +
FARK Drew Curtis' FARK.com +
FEED Magazine +
IA EH. Eleganthack is a guide to Information Architecture, Interface Design, Usability, User Ce +
Ironminds +
memepool.com +
Metafilter Community Weblog +
mrbarrett.com +
Plastic +
Salon.com +
SHARPEWORLD america's no. 1 website +
Slashdot News for nerds, stuff that matters +
Suck.com Daily +
Talk Hard Online Forums +
The Morning News +
TheStandard.com homepage +
Wired News +

+

Dell

+

+

Dell Auction +
Dell +
Dellnet +
Gigabuys +
Support.Dell.com +

+

eZines

+

+

Association of Alternative Newsweeklies +
LOST AT SEA online +
MotherJones.com -- News and Resources for the Skeptical Citizen +
Shift.com +

+

Finances

+

+

Fidelity Asset Manager Growth +
Fool.com Finance and Folly -- Main Page +
Fool.com Is Your 401(k) Foolish -- How to Pick a Winner +
IAParticipant +
My Fidelity +
The Motley Fool -- My Fool +
Welcome to Maverick Investing with Doug Fabian! +

+

Fonts

+

+

04 extra bitmap +
1001 Fonts .com +
BLAMBOT Comic Fonts & Lettering +
CHANK FONTS! +
CheapskateFonts - Fonts +
Dr. Design's Definitive Design Links List +
Font Collections in Specialty Themes +
font-o-ville +
Fontmaker [PAGE 1] +
Fontosaurus Text +
Fonts & Things - the most unusual fonts online... +
Free Typewriter Fonts +
Larabie Fonts +
Orgdot open source pixel and led fonts for flash +
phantomphonts +
Small Fonts by Cal Henderson +
The Dog Hause Fonts +
Web Page Design for Designers - Pixel Fonts +
www.pizzadude.dk fonts.html +

+

Funnies

+

+

SatireWire dot.com.edy +
SNL MM (Saturday Night Live Multimedia - snl.jt.org) YOUR SOURCE FOR SNL MULTIMEDIA, saturday n +
The Dialectizer +
Welcome to SNL InfoMedia +

+

Grateful Dead

+

+

(4442) Garcia (Star) +
(4442) Garcia +
A Long, Staid Trip - How Deadheads ruined the Grateful Dead. By Marc Weingarten +
Access Place Grateful Dead - Lyrics, Pictures, MP3, Setlists.. +
BEAR'S ART PAGES +
Betty Board info +
Blair Jackson - Garcia +
Buffalo News Concert Review 7-17-90 +
Concert Posters +
db.etree.org - GD by Year +
Deadhook @ www.ezboard.com +
deadlegs.com mp3 and real audio library of grateful dead quality audio. Mp3 shows and Mp3 bootl +
Deadshow.com +
Did You Know +
Doug Irwins First Amended Petition under Probate Code section 9860 +
Dozin.com +
Eurodead.net - for Grateful Dead fans everywhere +
Eurodead.net Links Directory +
Garcia's Guitars Mac Video Game +
GD CD-R Covers +
Google Search grateful dead 1-20-79 +
Google Search rec.music.gdead +
Google Web Directory - Arts Music Bands and Artists G Grateful Dead +
GRAMMY.com - Interview with Dennis McNally +
Grateful Day +
Grateful Dead Almanac +
GRATEFUL DEAD AT WINTERLAND, NEW YEAR'S 1972-73 +
Grateful Dead Family Discography +
Grateful Dead Frequently Asked Questions +
Grateful Dead Links +
GRATEFUL DEAD LIVE - www.gdlive.com +
Grateful Dead Lyric And Song Finder +
Guitar.com - Jerry Garcia The Acoustic Man's Dead +
How to 'Truck' the Brand Lessons from the Grateful Dead - Page 1 +
Jerry Garcias Guitars +
Lone Star Dead web space +
psilo.com - Dead Tickets, Passes, & Laminates +
Rock and Roll Hall of Fame and Museum Hall of Fame Inductee Detail +
RollingStone.com Artists The Grateful Dead +
SHN List for Grateful Dead +
Songs for the Dead +
Strings of gold - There was something special about Doug Irwin's guitars, and Jerry Garcia knew +
TBT Venues New York +
The Annotated Grateful Dead Lyrics, by David Dodd +
The DeadBoard Dead General Got something to talk about +
The Deadheads Newsletters 1972 -1974 +
The Grateful Dead - FBI Files +
The SetList Program +
The Smoking Gun Archive- Garcia's Assets +
TheStandard.com I'm With the Brand +
Uncle Sam and the Grateful Dead +
Wired.com - April 23, 2001 - Deadheads May Not Be Grateful +
www.gdlive.com - -shn- +

+

Indie Music Sites

+

+

--indietabs.net i can't freestyle. i'm a low budget ludacris. +
-30-music ... 30music.com +
3WK Underground Radio - Internet Only Alternative Radio! +
Aversion.com - Rock Punk Indie Music +
barsuk records +
CMJ New Music First +
Comes with a Smile +
Crud Magazine +
Delusions of Adequacy +
Dusted Magazine +
Emmie Magazine +
Epitonic.com Hi Quality Free MP3 Music +
http--www.basement-life.com- +
hybridmagazine.com +
In Music We Trust +
indieworkshop.com +
insound +
j u n k m e d i a music news, reviews & interviews +
L o s t A t S e a . o n l i n e +
Live Indie Rock Music +
mp3.palukaville.net +
P R O J E C T A T L A N T I S Z I N E +
Pitchforkmedia.com +
PopMatters +
Rocket Fuel Online Magazine +
SOUNDTHESIRENS.COM - Independent Online Magazine +
Splendid +
tonevendor.com . welcome +

+

Information Architecture

+

+

Argus Center for Information Architecture +

+

Jobs

+

+

BuffaloJobFinder.com +
GreatJobNetwork.com Jobs in Buffalo, New York +
HotJobs.com +
iambuffaloniagarajobs.com +
Western New York Jobs +

+

Lab

+

+

Black Lab Studios - Gallery Autumn +
Dog Links - E-commerce +
LUCKY LABRADOR BREWING COMPANY +

+

Links

+

+

3WK +
Admin +
BNews +
Board +
CDR +
Demon +
eBay +
Fark +
Google +
jay +
Lishost - Board +
M&T +
MindLeaders +
MSN +
MyFool +
MyUB +
NYT +
PHP +
PortalQA +
PP3 +
PP4 +
Setlist Corrections +
Slash +
TMD +

+

Media

+

+

Bloomberg +
Capitol Records +
CBS +
CNBC Dow Jones Business Video +
CNET Today - Technology News +
CNN Videoselect +
Disney +
ESPN Sports +
Fox News +
Fox Sports +
Hollywood Online +
Internet Radio Guide +
MSNBC +
MUSICVIDEOS.COM +
NBC VideoSeeker +
Sugarmegs Live Stream +
TV Guide Entertainment Network +
Universal Studios Online +
Warner Bros. Hip Clips +
What's On Now +
Windows Media Showcase +

+

Music

+

+

Digital Club Network (DCN) - The Live Music Source +
Epitonic.com Hi Quality Free MP3 Music +
Indie Live MP3s - mp3.palukaville.net +
Live Indie Rock Music +
Live365.com - Broadcast +
MP3.com Buffalo +
MusicToday +
Tim's Secret MP3 Stash +

+

Nice Sites

+

+

Art Technology Group +
Black Dog Interactive A New Breed +
Born Magazine Design and Literature Collaboration +
Brience.com +
Factiva, a Dow Jones Reuters Company +
FACTOR DESIGN +
Ford HEV +
fusionOne +
International Herald Tribune +
iSyndicate Syndication solutions built on our Intelligent Syndication Network +
oddcast +
siegelgale Launch +

+

PHP

+

+

codewalkers.com - main page - PHP Help +
EZwebdesign.com - PHP Resources +
HotScripts.com PHP +
http--www.phpinsider.com- +
Macromedia - PHP and Dreamweaver MX +
MySQL Documentation MySQL Commented MySQL Manual +
New York PHP - Linux Apache MySQL PHP +
PHP Hideout +
PHP Hypertext Preprocessor +
PHP Magazin - German +
PHP Manual date +
PHP WORLD - Free PHP Help , Resources and Scripts Consulting - MYSQL, Oracle database PHP Suppo +
PHP-Nuke +
PHPBuilder.com - The Resource For PHP Developers +
PHPCon East 2003 US Conference for PHP Developers. +
PHPDeveloper.org +
PHPkitchen - Come See What's Cookin' +
phpWizard - Building Dynamic Websites with PHP +
PX PHP Code Exchange +
Random CVS notes +
The PHP Resource Index +
WDVL PHP +
Web Design References PHP +
WebmasterBase - PHP and MySQL +

+

phpBB

+

+

Hacks.phpBB.Com +
phpBB 2 Index +
phpBB2 CVS snapshots +
phpBB2.de +
SourceForge Project Info - phpBB +

+

PhpNuke

+

+

PHP-Nuke +
Quebec Hardcore News +
Yahoo! Groups phpnuke Files +

+

References

+

+

AMG All Music Guide +
Bartleby.com Great Books Online +
CNNfyi.com - Student Mainpage +
Fast Facts Handbook +
homework help and educational resources at harcourt.com +
whatis.com +

+

Setlists

+

+

Allman Brothers Band Setlists +
Ben Harper Setlists +
Black Crows Setlists #1 +
Black Crows Setlists #2 +
Blues Traveler Setlists +
Blues Traveler Stage and Pre-show Setlists +
Bob Dylan Setlists +
Bob Marley Performances +
Bruce Hornsby Setlists +
Charlie Hunter Setlists +
Counting Crows Bootleg Guide +
Counting Crows Setlists - 2 +
Dark Star Orchestra Setlist +
Dave Matthews Band Setlists +
Derek Trucks Band +
Disco Biscuits Setlists +
Galactic Setlists +
Gov't Mule Setlists +
Grateful DeadLists +
Jane's Addiction - Boots +
Jazz Mandolin Project Setlists +
Jerry Garcia Setlists +
Jerry Joseph & the Jackmormons - Setlists +
Jimmy Buffet Setlists +
Karl Denson's Tiny Universe Setlists +
Led Zeppelin Setlists +
Leftover Salmon Setlists +
Modest Mouse Setlists +
Moe Setlists +
Other Ones Setlists +
Phil Lesh Setlists +
Pink Floyd Concert Appearances +
Pink Floyd ROIO +
Ratdog Setlists +
Rusted Root Setlists +
Samples Setlists +
Sector 9 Setlists +
Setlist.Com For all your setlist needs! +
Setlists - About.com +
Soulive Setlists +
Strangefolk Setlists +
String Cheese Setlists +
The Recipe Setlists +
U2 Setlist Archive +
Widespread Panic Setlists +

+

Shopping

+

+

Online DVD Deals and Coupons +
techbargains.com +

+

Steaming Tunes

+

+

3WK RealPlayer +
3WK Underground Radio +
Grateful Day +
Grateful Dead and Phriends- Streaming Real Audio Files +
Hip Boots - MP3 +
Hooked on Sonics Radio +
Listen +
musicneverstops.com +
OnShare GroupListing +
rip-off radio @ thebigripoff.com +
Sugarmegs- streams +
The Jam Zone! - Come in and groove with the Dead, Phish, Pink Floyd, Dave Matthews and many oth +

+

Trading

+

+

#tape_trade_central - Dalnet's Concert Trading Channel +
Bob Dylan Bootleg Artwork +
CD-R Trading Bootleg Cover Art Links +
CDR Covers - Yahoo! Photos - Thumbnails View +
CDR-Info, The Recording Authority +
CoverUniverse +
Etree.org +
etreenews.org +
Hip FTP +
http--www.u2-flom.de- +
i n h i d i n g  .  c o m +
Index of -etree +
Inlay Card Template +
Julian Fowler's CD Jewel Case Inserts +
NicksPicks.com +
Nothing's Shocking +
Paper CD Case +
Pink Floyd ROIO +
sativa.etree.org homepage +
Screech's Domain +
Shinburn - powered by XMB +
shntunes.org +
Tapers Revenge +
Templates CD-ROM Labels, CD Tray Cards, CD Inserts, and CD Mailers +
Welcome to Metropolis Noir - Your home for high quality Hip +

+

WAP-WML

+

+

allNetDevices -- The Wireless FAQ +
Nokia - WAP on Web +
Openwave Developer Program +
Openwave Systems Inc. +
Tag Reference - Wireless in a nutshell.com +
TagTag.com -+- FREE WAP site construction and hosting +
TTemulator - WAP emulator +
WAP (Wireless application protocol) - About.com +
WAP browser @ Gelon.net +
WAP Forum +
WAP Home - AnywhereYouGo.com +
WAP Usability Report Field Study Fall 2000 +
WAP.com - - Your guide to the wireless Internet, wap phones, wap services and PDAs +
WapTiger - WAP emulator +
Welcome to WAPDrive Free WAP sites WAP news WML tutorial WAP gateways +
Wireless Developer Network - Home +
Wireless in a nutshell.com - WAP, SMS, iMode, 3G, Bluetooth, VoxML, J2ME +
Wireless Industry - About.com +
WMLScript.com +
Yahoo! Mobile - Tours +
Yahoo! Mobile +

+

Web Design

+

+

404 Research Lab +
A List Apart For People Who Make Websites +
ASSEMBLER.ORG = Making Art With Machine Code; +
Bobby Validator +
Boxes and Arrows Because we can +
Chami.com - HTML-Kit, etc. +
CNET.com - Web Building - Stupid Web Tricks +
Color Schemer - Online Color Scheme Generator +
Contracts for every occasion - Web Building - CNET.com +
CSS Attributes Reference +
Doctor HTML v6 +
evolt.org Workers of the Web, Evolt! +
Flash 99% Good. First Aid Manual For Usable Flash Sites +
Frequent Questions Answered -- r937.com +
glish.com CSS layout techniques +
Graphic Design Resources Center - Graphic design tutorials, graphics tips, web design articles, +
Guide to Cascading Style Sheets +
GUIStuff.com - Free Graphical User Interfaces +
irt.org - JavaScript Windows FAQ Knowledge Base +
JavaScript Section - Homepage +
jjg.net information architecture resources +
Lighthouse latest +
LucDesk - Information Design . Web Usability . User Experience +
media inspiration +
more crayons colors for web designers +
my god...it's philippe starck... v-2 organisation +
Netscape - Developer +
O'Reilly Network Javascript and CSS DevCenter +
PDN's PIX - second site +
PDN's PIX - THE MAGAZINE FOR VISUAL CREATIVES +
Pirated Sites!! Aaarrgghh... +
Pirated Sites! +
Rabi's Dreamweaver Extensions +
Silicon Valley WebGuild Web Site Design Contest +
SimplytheBest Javascripts, JAVA Scripts +
SitePoint.com - Helping Business Grow Online! +
teamphotoshop.com +
The Web Standards Project Fighting for Standards in our Browsers +
Tomalak's Realm Daily Links to Strategic Web Design News +
Tutorials +
Usability.gov - Provided by the NCI National Cancer Institute +
Usable Web +
VisiBone Webmaster's Color Lab +
W3C HTML Validation Service +
W3Schools Online Web Tutorials +
Web Color Theory +
Web Design References +
Web Developer's Virtual Library Encyclopedia of Web Design Tutorials, Articles and Discussions +
Web Page Design for Designers - Home Page +
Web Pages That Suck +
Web Site Garage +
Web Tipes +
WebmasterBase - Helping Business Grow Online +
Webmonkey Reference Color Codes +
Webmonkey Reference Glossary +
Webmonkey Reference HTML Cheatsheet +
Webmonkey Reference Special Characters +
Webmonkey +
WebReview.com +
WebWord.com Hot Web Sites +
Welcome to SiteCritique.net - Helping You Create Better Designs Through Friendly Advice! +
Welcome to Web Resources - Where WebTechnology meets Simplicity +
ZDNet d e v e l o p e r Usability +

+

WNYMusic

+

+

Buffalo Music Online - Wnymusic.com +
Buffalo Music Online Message Board +
Ultimate Bulletin Board - Control Panel Frame +
WNYMusic - The List +

+

Zipzaps

+

+

Ebay - Items matching ( zipzap ) +
latencyproject.com - zip mods and racing +
Micro RC Cars Center +
Micro RC Cars Forum +
Radioshack.com - ZipZaps +
TinyRC.com - ZipZaps +
TinyRC.com +

+

.[DeskMod - Your Source for Desktop Modification]. +
ALT-PHP-FAQ - How do I turn newlines - returns into html breaks (br) +
BabyCentre Dads' stuff Diet for a healthy father-to-be +
BabyCentre Trying for a baby +
Business Week Online Salary Wizard +
Copyleft.net - Geek Chic! +
Coverall CD Audio Covers +
CVS--Concurrent Versions System - Table of Contents +
Dream Theater - Impaxx 5.18.93 - Dream Out Loud +
FastSubmit - Submit your website to search engines for FREE! +
Folding@home +
Free News Servers +
FreewarePalm +
History of Buffalo, New York +
internet beatles recording index main page +
irt.org - Dynamic HTML FAQ Knowledge Base +
irt.org - JavaScript Table FAQ Knowledge Base +
Keyboard Shortcuts for Windows +
Know Your Place! Shut Your Face! +
Lamb stuff! +
Misc. Space List +
MSN.com +
NASA's Visible Earth +
Opt-Out +
Portal Docs TOC +
Radio Station Guide +
Restoration Central Home, nurturing the passion for antiques and old homes with an Arts & Craft +
Rock and Roll Pumpkin Carving Patterns +
SternFanNetwork Home Page +
SWhois.net +
The Lawyers Who Get It Right - Jay Gerland +
ThinkGeek Stuff for Smart Masses +
US-NY-Buffalo-Web Developer +
WebMag Online - The No#1 Resource for Web Builders +
Welcome to the Happy Buffalo Slander Corner!!! +
Wxperience Forums - Forum Index +
[Real's JAVA JAVASCRIPT PB and WSH How-to] +

diff --git a/tnef-output.html b/tnef-output.html new file mode 100644 index 00000000..db591389 --- /dev/null +++ b/tnef-output.html @@ -0,0 +1,52 @@ + +TNEF Output + +

TNEF Parse Results

+

Source: local-data/winmail(1).dat (6143 bytes)

+

No attachments found.

+

HTML Body

+
+ +

Liebe Leserin, lieber Leser,

 

herzlichen Dank f�r Ihre Nachricht an die actimonda krankenkasse.

 

Um Ihre W�nsche k�mmern wir uns gewissenhaft und z�gig. Wir melden uns in K�rze bei Ihnen.

 

Ihre Anliegen sind herzlich willkommen. Zus�tzlich bieten wir Ihnen folgende Kontaktm�glichkeiten:

 

Tel.     0241 900 66-0

Fax.    0241 900 66-9100

Web   www.actimonda.de

FB      facebook.com/actimonda

 

Vielen Dank f�r Ihr Vertrauen. Alles Gute w�nscht Ihnen

 

Ihre

actimonda krankenkasse

+
+ \ No newline at end of file