- Implement TNEF extraction and recursive parsing in new `tnef_reader.go` and associated tests. - Create tests for TNEF extraction scenarios in `tnef_diag_test.go`, `tnef_diag7_test.go`, and `tnef_diag8_test.go`.
210 lines
5.3 KiB
Go
210 lines
5.3 KiB
Go
package internal
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/binary"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/teamwork/tnef"
|
|
)
|
|
|
|
func TestTNEFNestedMessage(t *testing.T) {
|
|
testFile := `H:\Dev\Gits\EMLy\EML_TNEF.eml`
|
|
if _, err := os.Stat(testFile); os.IsNotExist(err) {
|
|
t.Skip("test EML file not present")
|
|
}
|
|
|
|
f, _ := os.Open(testFile)
|
|
defer f.Close()
|
|
|
|
outerEmail, _ := Parse(f)
|
|
var innerData []byte
|
|
for _, att := range outerEmail.Attachments {
|
|
if strings.Contains(strings.ToLower(att.Filename), "postacert.eml") {
|
|
innerData, _ = io.ReadAll(att.Data)
|
|
break
|
|
}
|
|
}
|
|
|
|
innerEmail, _ := Parse(bytes.NewReader(innerData))
|
|
for _, att := range innerEmail.Attachments {
|
|
rawData, _ := io.ReadAll(att.Data)
|
|
if strings.ToLower(att.Filename) != "winmail.dat" {
|
|
continue
|
|
}
|
|
|
|
// Navigate to attAttachment (0x9005) for first attachment
|
|
offset := 6
|
|
for offset < len(rawData) {
|
|
if offset+9 > len(rawData) {
|
|
break
|
|
}
|
|
level := rawData[offset]
|
|
attrID := binary.LittleEndian.Uint32(rawData[offset+1 : offset+5])
|
|
attrLen := int(binary.LittleEndian.Uint32(rawData[offset+5 : offset+9]))
|
|
dataStart := offset + 9
|
|
|
|
if level == 0x02 && attrID == 0x00069005 && attrLen > 1000 {
|
|
mapiData := rawData[dataStart : dataStart+attrLen]
|
|
|
|
// Parse MAPI props to find PR_ATTACH_DATA_OBJ (0x3701)
|
|
embeddedData := extractPRAttachDataObj(mapiData)
|
|
if embeddedData == nil {
|
|
t.Fatal("could not find PR_ATTACH_DATA_OBJ")
|
|
}
|
|
|
|
fmt.Printf("PR_ATTACH_DATA_OBJ total: %d bytes\n", len(embeddedData))
|
|
fmt.Printf("First 32 bytes after GUID: %x\n", embeddedData[16:min2(48, len(embeddedData))])
|
|
|
|
// Check if after the 16-byte GUID there's a TNEF signature
|
|
afterGuid := embeddedData[16:]
|
|
if len(afterGuid) >= 4 {
|
|
sig := binary.LittleEndian.Uint32(afterGuid[0:4])
|
|
fmt.Printf("Signature after GUID: 0x%08X (TNEF=0x223E9F78)\n", sig)
|
|
|
|
if sig == 0x223E9F78 {
|
|
fmt.Println("It's a nested TNEF stream!")
|
|
decoded, err := tnef.Decode(afterGuid)
|
|
if err != nil {
|
|
fmt.Printf("Nested TNEF decode error: %v\n", err)
|
|
} else {
|
|
fmt.Printf("Nested Body: %d bytes\n", len(decoded.Body))
|
|
fmt.Printf("Nested BodyHTML: %d bytes\n", len(decoded.BodyHTML))
|
|
fmt.Printf("Nested Attachments: %d\n", len(decoded.Attachments))
|
|
for i, na := range decoded.Attachments {
|
|
fmt.Printf(" [%d] %q (%d bytes)\n", i, na.Title, len(na.Data))
|
|
}
|
|
fmt.Printf("Nested Attributes: %d\n", len(decoded.Attributes))
|
|
}
|
|
} else {
|
|
// Try as raw MAPI attributes (no TNEF wrapper)
|
|
fmt.Printf("Not a TNEF stream. First byte: 0x%02X\n", afterGuid[0])
|
|
// Check if it's a count of MAPI properties
|
|
if len(afterGuid) >= 4 {
|
|
propCount := binary.LittleEndian.Uint32(afterGuid[0:4])
|
|
fmt.Printf("First uint32 (possible prop count): %d\n", propCount)
|
|
}
|
|
}
|
|
}
|
|
break
|
|
}
|
|
|
|
offset += 9 + attrLen + 2
|
|
}
|
|
}
|
|
}
|
|
|
|
func extractPRAttachDataObj(mapiData []byte) []byte {
|
|
if len(mapiData) < 4 {
|
|
return nil
|
|
}
|
|
count := int(binary.LittleEndian.Uint32(mapiData[0:4]))
|
|
offset := 4
|
|
|
|
for i := 0; i < count && offset+4 <= len(mapiData); i++ {
|
|
propTag := binary.LittleEndian.Uint32(mapiData[offset : offset+4])
|
|
propType := propTag & 0xFFFF
|
|
propID := (propTag >> 16) & 0xFFFF
|
|
offset += 4
|
|
|
|
// Handle named props
|
|
if propID >= 0x8000 {
|
|
if offset+20 > len(mapiData) {
|
|
return nil
|
|
}
|
|
kind := binary.LittleEndian.Uint32(mapiData[offset+16 : offset+20])
|
|
offset += 20
|
|
if kind == 0 {
|
|
offset += 4
|
|
} else {
|
|
if offset+4 > len(mapiData) {
|
|
return nil
|
|
}
|
|
nameLen := int(binary.LittleEndian.Uint32(mapiData[offset : offset+4]))
|
|
offset += 4 + nameLen
|
|
if nameLen%4 != 0 {
|
|
offset += 4 - nameLen%4
|
|
}
|
|
}
|
|
}
|
|
|
|
switch propType {
|
|
case 0x0002: // PT_SHORT
|
|
offset += 4
|
|
case 0x0003: // PT_LONG
|
|
offset += 4
|
|
case 0x000B: // PT_BOOLEAN
|
|
offset += 4
|
|
case 0x0040: // PT_SYSTIME
|
|
offset += 8
|
|
case 0x001E, 0x001F: // PT_STRING8, PT_UNICODE
|
|
if offset+4 > len(mapiData) {
|
|
return nil
|
|
}
|
|
cnt := int(binary.LittleEndian.Uint32(mapiData[offset : offset+4]))
|
|
offset += 4
|
|
for j := 0; j < cnt; j++ {
|
|
if offset+4 > len(mapiData) {
|
|
return nil
|
|
}
|
|
slen := int(binary.LittleEndian.Uint32(mapiData[offset : offset+4]))
|
|
offset += 4 + slen
|
|
if slen%4 != 0 {
|
|
offset += 4 - slen%4
|
|
}
|
|
}
|
|
case 0x0102: // PT_BINARY
|
|
if offset+4 > len(mapiData) {
|
|
return nil
|
|
}
|
|
cnt := int(binary.LittleEndian.Uint32(mapiData[offset : offset+4]))
|
|
offset += 4
|
|
for j := 0; j < cnt; j++ {
|
|
if offset+4 > len(mapiData) {
|
|
return nil
|
|
}
|
|
blen := int(binary.LittleEndian.Uint32(mapiData[offset : offset+4]))
|
|
offset += 4 + blen
|
|
if blen%4 != 0 {
|
|
offset += 4 - blen%4
|
|
}
|
|
}
|
|
case 0x000D: // PT_OBJECT
|
|
if offset+4 > len(mapiData) {
|
|
return nil
|
|
}
|
|
cnt := int(binary.LittleEndian.Uint32(mapiData[offset : offset+4]))
|
|
offset += 4
|
|
for j := 0; j < cnt; j++ {
|
|
if offset+4 > len(mapiData) {
|
|
return nil
|
|
}
|
|
olen := int(binary.LittleEndian.Uint32(mapiData[offset : offset+4]))
|
|
offset += 4
|
|
if propID == 0x3701 {
|
|
// This is PR_ATTACH_DATA_OBJ!
|
|
return mapiData[offset : offset+olen]
|
|
}
|
|
offset += olen
|
|
if olen%4 != 0 {
|
|
offset += 4 - olen%4
|
|
}
|
|
}
|
|
default:
|
|
return nil
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func min2(a, b int) int {
|
|
if a < b {
|
|
return a
|
|
}
|
|
return b
|
|
}
|