/*
  Copyright (C) 2021 Inverse inc.

  This file is part of SOGo.

  SOGo is free software; you can redistribute it and/or modify it under
  the terms of the GNU Lesser General Public License as published by the
  Free Software Foundation; either version 2, or (at your option) any
  later version.

  OGo is distributed in the hope that it will be useful, but WITHOUT ANY
  WARRANTY; without even the implied warranty of MERCHANTABILITY or
  FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public
  License for more details.

  You should have received a copy of the GNU Lesser General Public
  License along with OGo; see the file COPYING.  If not, write to the
  Free Software Foundation, 59 Temple Place - Suite 330, Boston, MA
  02111-1307, USA.

  The following code is a derivative work of the code from the Yerase's TNEF
  Stream Reader which is licensed GPLv2.
*/

#import <Foundation/NSArray.h>
#import <Foundation/NSDictionary.h>
#import <Foundation/NSValue.h>

#import <NGExtensions/NGHashMap.h>
#import <NGExtensions/NSObject+Logs.h>

#import <NGMime/NGMimeBodyPart.h>
#import <NGMime/NGMimeFileData.h>
#import <NGMime/NGMimeHeaderFields.h>
#import <NGMime/NGMimeMultipartBody.h>

#import <SOGo/RTFHandler.h>
#import <SOGo/SOGoBuild.h>
#import <SOGo/SOGoSystemDefaults.h>

#import <SoObjects/Mailer/NSData+SMIME.h>

#import <ytnef.h>

#import "SOGoTNEFMailBodyPart.h"

#define UPR_TO_ATTENDEES_STRING  0x823B
#define UPR_CC_ATTENDEES_STRING  0x823C
#define UPR_ALL_ATTENDEES_STRING 0x8238

/*
  SOGoTNEFMailBodyPart

  A specialized SOGoMailBodyPart subclass for application/ms-tnef attachments. Can
  be used to attach special SoMethods.

  See the superclass for more information on part objects.
*/

unsigned char GetRruleCount(unsigned char a, unsigned char b) {
  return ((a << 8) | b);
}

char *GetRruleDayname(unsigned char a) {
  static char daystring[25];

  *daystring = 0;

  if (a & 0x01) {
    strcat(daystring, "SU,");
  }
  if (a & 0x02) {
    strcat(daystring, "MO,");
  }
  if (a & 0x04) {
    strcat(daystring, "TU,");
  }
  if (a & 0x08) {
    strcat(daystring, "WE,");
  }
  if (a & 0x10) {
    strcat(daystring, "TH,");
  }
  if (a & 0x20) {
    strcat(daystring, "FR,");
  }
  if (a & 0x40) {
    strcat(daystring, "SA,");
  }

  if (strlen(daystring)) {
    daystring[strlen(daystring) - 1] = 0;
  }

  return (daystring);
}

unsigned char GetRruleMonthNum(unsigned char a, unsigned char b) {
  switch (a) {
    case 0x00:
      switch (b) {
        case 0x00:
          // Jan
          return (1);
        case 0xA3:
          // May
          return (5);
        case 0xAE:
          // Nov
          return (11);
      }
      break;
    case 0x60:
      switch (b) {
        case 0xAE:
          // Feb
          return (2);
        case 0x51:
          // Jun
          return (6);
      }
      break;
    case 0xE0:
      switch (b) {
        case 0x4B:
          // Mar
          return (3);
        case 0x56:
          // Sep
          return (9);
      }
      break;
    case 0x40:
      switch (b) {
        case 0xFA:
          // Apr
          return (4);
      }
      break;
    case 0x20:
      if (b == 0xFA) {
        // Jul
        return (7);
      }
      break;
    case 0x80:
      if (b == 0xA8) {
        // Aug
        return (8);
      }
      break;
    case 0xA0:
      if (b == 0xFF) {
        // Oct
        return (10);
      }
      break;
    case 0xC0:
      if (b == 0x56) {
        return (12);
      }
  }

  // Error
  return (0);
}

@implementation SOGoTNEFMailBodyPart

/* Overwritten methods */

- (id) init
{
  if ((self = [super init]))
    {
      debugOn = [[SOGoSystemDefaults sharedSystemDefaults] tnefDecoderDebugEnabled];
      part = nil;
      filename = nil;
      bodyParts = [[NGMimeMultipartBody alloc] init];
    }

  return self;
}

- (id) initWithName: (NSString *) _name
        inContainer: (id) _container
{
  self = [super initWithName: _name inContainer: _container];

  [self decodeBLOB];

  return self;
}

- (void) dealloc
{
  [part release];
  [filename release];
  [bodyParts release];

  [super dealloc];
}

- (id) lookupName: (NSString *) _key
	inContext: (id) _ctx
	  acquire: (BOOL) _flag
{
  NSArray *parts;
  int i;

  if ([self isBodyPartKey: _key])
    {
      // _key is an integer
      parts = [bodyParts parts];
      i = [_key intValue] - 1;

      if (i > -1 && i < [parts count])
        {
          [self setPart: [parts objectAtIndex: i]];
          return self;
        }
    }
  else if ([_key isEqualToString: [self filename]])
    {
      return self;
    }
  else if ([_key isEqualToString: @"asAttachment"])
    {
      [self setAsAttachment];
      return self;
    }

  /* Fallback to super class */
  return [super lookupName: _key inContext: _ctx acquire: _flag];
}

- (NSData *) fetchBLOB
{
  if (part)
    return [part body];

  return [super fetchBLOB];
}

- (NSString *) filename
{
  if (filename)
    return filename;
  else if (part)
    return nil; // don't try to fetch the filename from the IMAP body structure
  else
    return [super filename];
}

- (id) partInfo
{
  if (partInfo)
    return partInfo;
  else if (part)
    return nil; // don't try to fetch the info from the IMAP body structure
  else
    return [super partInfo];
}

/* New methods */

- (NGMimeMultipartBody *) bodyParts
{
  return bodyParts;
}

- (void) decodeBLOB
{
  NSData *data;
  NSEnumerator *list;
  NSString *messageClass;
  NSString *partName, *type, *subtype;
  NSString *value, *attendee;
  RTFHandler *handler;
  NGMimeBodyPart* eventPart;

  DWORD signature;
  DDWORD *classification;
  dtr datetime;
  TNEFStruct tnef;
  variableLength *attachmentName;
  variableLength *filedata;
  variableLength buf;

  [self setPart: nil];
  partName = nil;
  data = [self fetchBLOB];

  if((int)[data length] < sizeof(DWORD)) {
    NSLog(@"TNEF: ERROR try to decode empty data where it should'nt");
    return;
  }
    
  memcpy(&signature, [data bytes], sizeof(DWORD));
  if (TNEFCheckForSignature(signature) == 0)
    {
      TNEFInitialize(&tnef);
      tnef.Debug = 0;
      if (TNEFParseMemory((unsigned char *)[data bytes], [data length], &tnef) != -1)
        {
          messageClass = [NSString stringWithCString: tnef.messageClass];

          if (debugOn)
            {
              NSLog(@"TNEF message class: %@", messageClass);
              MAPIPrint(&tnef.MapiProperties);
            }

          if ([messageClass isEqualToString: @"IPM.Microsoft Mail.Note"])
            {
              if (tnef.subject.size > 0)
                {
                  data = nil;
                  filedata = MAPIFindProperty(&(tnef.MapiProperties), PROP_TAG(PT_BINARY, PR_BODY_HTML));
                  if (filedata != MAPI_UNDEFINED)
                    {
                      partName = [NSString stringWithFormat: @"%s.html", tnef.subject.data];
                      data = [NSData dataWithBytes: filedata->data length: filedata->size];
                    }
                  else
                    {
                      filedata = MAPIFindProperty(&(tnef.MapiProperties), PROP_TAG(PT_BINARY, PR_RTF_COMPRESSED));
                      if (filedata != MAPI_UNDEFINED)
                        {
                          buf.data = DecompressRTF(filedata, &(buf.size));
                          if (buf.data != NULL)
                            {
                              partName = [NSString stringWithFormat: @"%s.html", tnef.subject.data];
                              data = [NSData dataWithBytes: buf.data length: buf.size];

                              handler = [[RTFHandler alloc] initWithData: data];
                              AUTORELEASE(handler);
                              data = [handler parse]; // RTF to HTML
                            }
                        }
                    }

                  if ([data length])
                    {
                      [self bodyPartForData: data
                                   withType: @"text"
                                 andSubtype: @"html"];
                    }
                }
            }
          else if ([messageClass isEqualToString: @"IPM.Microsoft Schedule.MtgRespA"] || // tentative response
                   [messageClass isEqualToString: @"IPM.Microsoft Schedule.MtgRespP"] || // positive (accepted) response
                   [messageClass isEqualToString: @"IPM.Microsoft Schedule.MtgRespN"] || // negative (declined) response
                   [messageClass isEqualToString: @"IPM.Microsoft Schedule.MtgReq"])     // request (invitation)
            {
              // Meeting object -- construct text/calendar part

              // Parse HTML body, if any
              filedata = MAPIFindProperty(&(tnef.MapiProperties), PROP_TAG(PT_BINARY, PR_BODY_HTML));
              if (filedata != MAPI_UNDEFINED && filedata->size > 0)
                {
                  partName = [NSString stringWithFormat: @"%s.html", tnef.subject.data];
                  data = [NSData dataWithBytes: filedata->data length: filedata->size];
                  [self bodyPartForData: data
                               withType: @"text"
                             andSubtype: @"html"];
                }

              // Create ics attachment
              NSMutableString *vcalendar = [NSMutableString stringWithString: @"BEGIN:VCALENDAR\n"];
              BOOL isRequest = [messageClass isEqualToString: @"IPM.Microsoft Schedule.MtgReq"];

              if (isRequest)
                [vcalendar appendString: @"METHOD:REQUEST\n"];
              else
                [vcalendar appendString: @"METHOD:REPLY\n"];
              [vcalendar appendFormat: @"PRODID:-//Inverse inc./SOGo %@//EN\n", SOGoVersion];
              [vcalendar appendString: @"VERSION:2.0\n"];
              [vcalendar appendString: @"BEGIN:VEVENT\n"];

              // UID
              // TODO: Probably wrong, probably irrelevant
              filedata = MAPIFindUserProp(&(tnef.MapiProperties), PROP_TAG(PT_BINARY, 0x3));
              if (filedata == MAPI_UNDEFINED)
                filedata = MAPIFindUserProp(&(tnef.MapiProperties), PROP_TAG(PT_BINARY, 0x23));
              if (filedata != MAPI_UNDEFINED && filedata->size > 1)
                {
                  int i;
                  [vcalendar appendString: @"UID:"];
                  for (i = 0; i < filedata->size; i++)
                    {
                      [vcalendar appendFormat: @"%02X", (unsigned char)filedata->data[i]];
                    }
                  [vcalendar appendString: @"\n"];
                }

              // Sequence
              filedata = MAPIFindUserProp(&(tnef.MapiProperties), PROP_TAG(PT_LONG, 0x8201));
              if (filedata != MAPI_UNDEFINED)
                {
                  [vcalendar appendFormat: @"SEQUENCE:%i\n", (int)*filedata->data];
                }

              // Attendee email
              if (isRequest)
                {
                  // Organizer
                  filedata = MAPIFindProperty(&(tnef.MapiProperties), PROP_TAG(PT_BINARY, PR_SENDER_SEARCH_KEY));
                  if (filedata == MAPI_UNDEFINED)
                    {
                      filedata = MAPIFindProperty(&(tnef.MapiProperties), PROP_TAG(PT_UNICODE, PR_SENT_REPRESENTING_EMAIL_ADDRESS));
                    }
                  if (filedata != MAPI_UNDEFINED && filedata->size > 1)
                    {
                      NSArray *components;
                      NSString *email, *cn;
                      email = [NSString stringWithUTF8String: (const char *)filedata->data];
                      components = [email componentsSeparatedByString: @":"];
                      email = [components objectAtIndex: 0];

                      if ([components count] > 1)
                        cn = [components objectAtIndex: 1];
                      else
                        {
                          filedata = MAPIFindProperty(&(tnef.MapiProperties), PROP_TAG(PT_UNICODE, PR_SENT_REPRESENTING_NAME));
                          if (filedata != MAPI_UNDEFINED && filedata->size > 1)
                            cn = [NSString stringWithUTF8String: (const char *)filedata->data];
                          else
                            cn = email;
                        }
                      [vcalendar appendFormat: @"ORGANIZER;cn=\"%@\":mailto:%@\n", cn, email];
                    }

                  // Attendees
                  filedata = MAPIFindUserProp(&(tnef.MapiProperties), PROP_TAG(PT_STRING8, UPR_TO_ATTENDEES_STRING));
                  if (filedata == MAPI_UNDEFINED)
                    {
                      filedata = MAPIFindUserProp(&(tnef.MapiProperties), PROP_TAG(PT_UNICODE, UPR_TO_ATTENDEES_STRING));
                    }
                  if (filedata == MAPI_UNDEFINED)
                    {
                      filedata = MAPIFindUserProp(&(tnef.MapiProperties), PROP_TAG(PT_STRING8, UPR_ALL_ATTENDEES_STRING));
                    }
                  if (filedata == MAPI_UNDEFINED)
                    {
                      filedata = MAPIFindUserProp(&(tnef.MapiProperties), PROP_TAG(PT_UNICODE, UPR_ALL_ATTENDEES_STRING));
                    }
                  if (filedata != MAPI_UNDEFINED && filedata->size > 1)
                    {
                      // Required attendees
                      value = [NSString stringWithUTF8String: (const char *)filedata->data];
                      list = [[value componentsSeparatedByString: @";"] objectEnumerator];
                      while ((attendee = [list nextObject]))
                        {
                          attendee = [attendee stringByTrimmingSpaces];
                          [vcalendar appendFormat: @"ATTENDEE;PARTSTAT=NEEDS-ACTION;ROLE=REQ-PARTICIPANT;RSVP=TRUE;CN=\"%@\":MAILTO:%@\n",
                                     attendee, attendee];
                        }

                      // Optional attendees
                      filedata = MAPIFindUserProp(&(tnef.MapiProperties), PROP_TAG(PT_STRING8, UPR_CC_ATTENDEES_STRING));
                      if (filedata == MAPI_UNDEFINED)
                        {
                          filedata = MAPIFindUserProp(&(tnef.MapiProperties), PROP_TAG(PT_UNICODE, UPR_CC_ATTENDEES_STRING));
                        }
                      if (filedata != MAPI_UNDEFINED && filedata->size > 1)
                        {
                          value = [NSString stringWithUTF8String: (const char *)filedata->data];
                          if ([value length])
                            {
                              list = [[value componentsSeparatedByString: @";"] objectEnumerator];
                              while ((attendee = [list nextObject]))
                                {
                                  attendee = [attendee stringByTrimmingSpaces];
                                  [vcalendar appendFormat: @"ATTENDEE;PARTSTAT=NEEDS-ACTION;ROLE=OPT-PARTICIPANT;RSVP=TRUE;CN=\"%@\":MAILTO:%@\n",
                                             attendee, attendee];
                                }
                            }
                        }
                    }
                }
              else
                {
                  // Meeting response
                  filedata = MAPIFindProperty(&(tnef.MapiProperties), PROP_TAG(PT_UNICODE, PR_SENT_REPRESENTING_EMAIL_ADDRESS));
                  if (filedata == MAPI_UNDEFINED)
                    {
                      filedata = MAPIFindProperty(&(tnef.MapiProperties), PROP_TAG(PT_UNICODE, PR_SENDER_SMTP_ADDRESS));
                    }
                  if (filedata != MAPI_UNDEFINED && filedata->size > 1)
                    {
                      NSString *email, *cn, *partstat;
                      email = [NSString stringWithUTF8String: (const char *)filedata->data];
                      filedata = MAPIFindProperty(&(tnef.MapiProperties), PROP_TAG(PT_UNICODE, PR_SENT_REPRESENTING_NAME));
                      if (filedata == MAPI_UNDEFINED)
                        {
                          filedata = MAPIFindProperty(&(tnef.MapiProperties), PROP_TAG(PT_UNICODE, PR_SENDER_NAME));
                        }
                      if (filedata != MAPI_UNDEFINED && filedata->size > 1)
                        cn = [NSString stringWithUTF8String: (const char *)filedata->data];
                      else
                        cn = email;

                      switch ([messageClass characterAtIndex: [messageClass length] - 1])
                        {
                        case 'N':
                          partstat = @"DECLINED";
                          break;
                        case 'A':
                          partstat = @"TENTATIVE";
                          break;
                        default:
                          partstat = @"ACCEPTED";
                        }
                      [vcalendar appendFormat: @"ATTENDEE;PARTSTAT=%@;CN=\"%@\":MAILTO:%@\n", partstat, cn, email];
                    }
                }

              // Summary
              filedata = MAPIFindProperty(&(tnef.MapiProperties), PROP_TAG(PT_STRING8, PR_CONVERSATION_TOPIC));
              if (filedata == MAPI_UNDEFINED)
                {
                  filedata = MAPIFindProperty(&(tnef.MapiProperties), PROP_TAG(PT_UNICODE, PR_CONVERSATION_TOPIC));
                }
              if (filedata != MAPI_UNDEFINED && filedata->size > 1)
                {
                  [vcalendar appendFormat: @"SUMMARY:%@\n", [NSString stringWithUTF8String: (const char *)filedata->data]];
                }

              // Description
              filedata = MAPIFindProperty(&(tnef.MapiProperties), PROP_TAG(PT_STRING8, 0x3fd9));
              if (filedata == MAPI_UNDEFINED)
                {
                  filedata = MAPIFindProperty(&(tnef.MapiProperties), PROP_TAG(PT_UNICODE, 0x3fd9));
                }
              if (filedata != MAPI_UNDEFINED && filedata->size > 1)
                {
                  [vcalendar appendFormat: @"DESCRIPTION:%@\n", [NSString stringWithUTF8String: (const char *)filedata->data]];
                }

              // Location
              filedata = MAPIFindUserProp(&(tnef.MapiProperties), PROP_TAG(PT_STRING8, 0x0002));
              if (filedata == MAPI_UNDEFINED)
                filedata = MAPIFindUserProp(&(tnef.MapiProperties), PROP_TAG(PT_STRING8, 0x8208));
              if (filedata == MAPI_UNDEFINED)
                filedata = MAPIFindUserProp(&(tnef.MapiProperties), PROP_TAG(PT_UNICODE, 0x0002));
              if (filedata == MAPI_UNDEFINED)
                filedata = MAPIFindUserProp(&(tnef.MapiProperties), PROP_TAG(PT_UNICODE, 0x8208));
              if (filedata != MAPI_UNDEFINED && filedata->size > 1)
                {
                  [vcalendar appendFormat: @"LOCATION: %@\n", [NSString stringWithUTF8String: (const char *)filedata->data]];
                }

              // Date start
              filedata = MAPIFindUserProp(&(tnef.MapiProperties), PROP_TAG(PT_SYSTIME, 0x820d));
              if (filedata == MAPI_UNDEFINED)
                filedata = MAPIFindUserProp(&(tnef.MapiProperties), PROP_TAG(PT_SYSTIME, 0x8516));
              if (filedata != MAPI_UNDEFINED && filedata->size > 1)
                {
                  MAPISysTimetoDTR(filedata->data, &datetime);
                  [vcalendar appendFormat: @"DTSTART:%04i%02i%02iT%02i%02i%02iZ\n",
                        datetime.wYear, datetime.wMonth, datetime.wDay, datetime.wHour, datetime.wMinute, datetime.wSecond];
                }

              // Date end
              filedata = MAPIFindUserProp(&(tnef.MapiProperties), PROP_TAG(PT_SYSTIME, 0x820e));
              if (filedata == MAPI_UNDEFINED)
                filedata = MAPIFindUserProp(&(tnef.MapiProperties), PROP_TAG(PT_SYSTIME, 0x8517));
              if (filedata != MAPI_UNDEFINED && filedata->size > 1)
                {
                  MAPISysTimetoDTR(filedata->data, &datetime);
                  [vcalendar appendFormat: @"DTEND:%04i%02i%02iT%02i%02i%02iZ\n",
                        datetime.wYear, datetime.wMonth, datetime.wDay, datetime.wHour, datetime.wMinute, datetime.wSecond];
                }

              // Date stamp
              filedata = MAPIFindUserProp(&(tnef.MapiProperties), PROP_TAG(PT_SYSTIME, 0x8202));
              if (filedata == MAPI_UNDEFINED)
                filedata = MAPIFindUserProp(&(tnef.MapiProperties), PROP_TAG(PT_SYSTIME, 0x001a));
              if (filedata != MAPI_UNDEFINED && filedata->size > 1)
                {
                  MAPISysTimetoDTR(filedata->data, &datetime);
                  [vcalendar appendFormat: @"DTSTAMP:%04i%02i%02iT%02i%02i%02iZ\n",
                        datetime.wYear, datetime.wMonth, datetime.wDay, datetime.wHour, datetime.wMinute, datetime.wSecond];
                }

              // Classification
              filedata = MAPIFindUserProp(&(tnef.MapiProperties), PROP_TAG(PT_BOOLEAN, 0x8506));
              if (filedata != MAPI_UNDEFINED)
                {
                  classification = (DDWORD *)filedata->data;
                  if (*classification == 1)
                    [vcalendar appendString: @"CLASS:PRIVATE\n"];
                  else
                    [vcalendar appendString: @"CLASS:PUBLIC\n"];
                }

              // Repeating rule
              filedata = MAPIFindUserProp(&(tnef.MapiProperties), PROP_TAG(PT_BINARY, 0x8216));
              if (filedata != MAPI_UNDEFINED && filedata->size >= 0x1F)
                {
                  NSMutableString *rrule = [NSMutableString string];
                  unsigned char *recurData = filedata->data;

                  // [vcalendar appendString: @"RRULE:FREQ="];
                  if (recurData[0x04] == 0x0A)
                    {
                      [rrule appendString: @"DAILY"];
                      if (recurData[0x16] == 0x23 || recurData[0x16] == 0x22 || recurData[0x16] == 0x21)
                        {
                          filedata = MAPIFindUserProp(&(tnef.MapiProperties), PROP_TAG(PT_I2, 0x0011));
                          if (filedata != MAPI_UNDEFINED)
                            [rrule appendFormat: @";INTERVAL=%d", *(filedata->data)];
                          if (recurData[0x16] == 0x22 || recurData[0x16] == 0x21)
                            [rrule appendFormat: @";COUNT=%d", GetRruleCount(recurData[0x1B], recurData[0x1A])];
                        }
                      else if (recurData[0x16] == 0x3E)
                        {
                          [rrule appendString: @";BYDAY=MO,TU,WE,TH,FR"];
                          if (recurData[0x1A] == 0x22 || recurData[0x1A] == 0x21)
                            [rrule appendFormat: @";COUNT=%d", GetRruleCount(recurData[0x1F], recurData[0x1E])];

                        }
                    }
                  else if (recurData[0x04] == 0x0B)
                    {
                      [rrule appendFormat: @"WEEKLY;INTERVAL=%d;BYDAY=%s", recurData[0x0E], GetRruleDayname(recurData[0x16])];
                      if (recurData[0x1A] == 0x22 || recurData[0x1A] == 0x21)
                        [rrule appendFormat: @";COUNT=%d", GetRruleCount(recurData[0x1F], recurData[0x1E])];
                    }
                  else if (recurData[0x04] == 0x0C)
                    {
                      [rrule appendString: @"MONTHLY"];
                      if (recurData[0x06] == 0x02)
                        {
                          [rrule appendFormat: @";INTERVAL=%d;BYMONTHDAY=%d", recurData[0x0E], recurData[0x16]];
                          if (recurData[0x1A] == 0x22 || recurData[0x1A] == 0x21)
                            [rrule appendFormat: @";COUNT=%d", GetRruleCount(recurData[0x1F], recurData[0x1E])];
                        }
                      else if (recurData[0x06] == 0x03)
                        {
                          [rrule appendFormat: @";BYDAY=%s;BYSETPOS=%d;INTERVAL=%d",
                                     GetRruleDayname(recurData[0x16]),
                                     recurData[0x1A] == 0x05 ? -1 : recurData[0x1A],
                                     recurData[0x0E]];
                          if (recurData[0x1E] == 0x22 || recurData[0x1E] == 0x21)
                            [rrule appendFormat: @";COUNT=%d", GetRruleCount(recurData[0x23], recurData[0x22])];
                        }
                    }
                  else if (recurData[0x04] == 0x0D)
                    {
                      [rrule appendFormat: @"YEARLY;BYMONTH=%d", GetRruleMonthNum(recurData[0x0A], recurData[0x0B])];
                      if (recurData[0x06] == 0x02)
                          [rrule appendFormat: @";BYMONTHDAY=%d", recurData[0x16]];
                      else if (recurData[0x06] == 0x03)
                        [rrule appendFormat: @";BYDAY=%s;BYSETPOS=%d",
                               GetRruleDayname(recurData[0x16]),
                               recurData[0x1A] == 0x05 ? -1 : recurData[0x1A]];
                      if (recurData[0x1E] == 0x22 || recurData[0x1E] == 0x21)
                        [rrule appendFormat: @";COUNT=%d", GetRruleCount(recurData[0x23], recurData[0x22])];
                    }

                  if ([rrule length])
                    [vcalendar appendFormat: @"RRULE:FREQ=%@\n", rrule];
                }

              [vcalendar appendString: @"END:VEVENT\n"];
              [vcalendar appendString: @"END:VCALENDAR"];

              if (debugOn)
                NSLog(@"TNEF reconstructed vCalendar:\n%@", vcalendar);

              eventPart = [self bodyPartForData: [vcalendar dataUsingEncoding: NSUTF8StringEncoding]
                           withType: @"text"
                         andSubtype: @"calendar"];
              [self setPart: eventPart];

            }
          // Other classes to handle:
          //
          //   IPM.Contact
          //   IPM.Task
          //   IPM.Microsoft Schedule.MtgCncl
          //   IPM.Appointment
          //

          Attachment *p;
          BOOL isObject, isRealAttachment;

          p = tnef.starting_attach.next;
          while (p != NULL)
            {
              if (p->FileData.size > 0)
                {
                  isObject = YES;

                  // See if the contents are stored as "attached data" inside the MAPI blocks.
                  filedata = MAPIFindProperty(&(p->MAPI), PROP_TAG(PT_OBJECT, PR_ATTACH_DATA_OBJ));
                  if (filedata == MAPI_UNDEFINED)
                    {
                      // Nope, standard TNEF stuff.
                      filedata = &(p->FileData);
                      isObject = NO;
                    }

                  // See if this is an embedded TNEF stream.
                  isRealAttachment = YES;

                  TNEFStruct emb_tnef;
                  DWORD object_signature;

                  if (isObject)
                    {
                      // This is an "embedded object", so skip the 16-byte identifier first.
                      if (NULL != filedata->data) {
                        memcpy(&object_signature, filedata->data + 16, sizeof(DWORD));
                        if (TNEFCheckForSignature(object_signature) == 0) {
                          TNEFInitialize(&emb_tnef);
                          emb_tnef.Debug = tnef.Debug;
                          if (TNEFParseMemory(filedata->data + 16, filedata->size - 16, &emb_tnef) != -1)
                            {
                              isRealAttachment = NO;
                            }
                          TNEFFree(&emb_tnef);
                       }
                      }
                    }
                  else
                    {
                      memcpy(&object_signature, filedata->data, sizeof(DWORD));
                      if (TNEFCheckForSignature(object_signature) == 0) {
                        TNEFInitialize(&emb_tnef);
                        emb_tnef.Debug = tnef.Debug;
                        if (TNEFParseMemory(filedata->data, filedata->size, &emb_tnef) != -1)
                          {
                            isRealAttachment = NO;
                          }
                        TNEFFree(&emb_tnef);
                      }
                    }
                  if (isRealAttachment)
                    {
                      // Ok, it's not an embedded stream, so now we process it.
                      attachmentName = MAPIFindProperty(&(p->MAPI), PROP_TAG(PT_STRING8, PR_ATTACH_LONG_FILENAME));
                      if (attachmentName == MAPI_UNDEFINED)
                        {
                          attachmentName = MAPIFindProperty(&(p->MAPI), PROP_TAG(PT_STRING8, PR_DISPLAY_NAME));
                          if (attachmentName == MAPI_UNDEFINED)
                            {
                              attachmentName = MAPIFindProperty(&(p->MAPI), PROP_TAG(PT_STRING8, PR_ATTACH_TRANSPORT_NAME));
                              if (attachmentName == MAPI_UNDEFINED)
                                {
                                  attachmentName = &(p->Title);
                                }
                            }
                        }

                      // MAPIPrint(&p->MAPI);

                      variableLength *prop;

                      if (attachmentName->size > 1)
                        {
                          partName = [NSString stringWithUTF8String: (const char *)attachmentName->data];

                          if (!partName) {
                            // Switch to different encoding
                            partName = [[NSString alloc] initWithData: [NSData dataWithBytes: attachmentName->data length: strlen(attachmentName->data)] encoding:NSISOLatin1StringEncoding];
                            [partName autorelease];
                          }
                          if (!partName) {
                            // Fallback
                            [self warnWithFormat: @"Could not get TNEF attachment name, fallback to unamed.bin"];
                            partName = @"unnamed.bin";
                          }

                          type = @"application";
                          subtype = @"octet-stream";

                          prop = MAPIFindProperty(&(p->MAPI), PROP_TAG(PT_UNICODE, PR_ATTACH_MIME_TAG));
                          if (prop != MAPI_UNDEFINED)
                            {
                              NSString *mime = [NSString stringWithUTF8String: (const char *)prop->data];
                              NSArray *pair = [mime componentsSeparatedByString: @"/"];
                              if ([pair count] == 2)
                                {
                                  type = [pair objectAtIndex: 0];
                                  subtype = [pair objectAtIndex: 1];
                                }
                              else
                                {
                                  [self warnWithFormat: @"Unexpected MIME type %@", mime];
                                }
                            }
                          else
                            {
                              prop = MAPIFindProperty(&(p->MAPI), PROP_TAG(PT_STRING8, PR_ATTACH_EXTENSION));
                              if (prop != MAPI_UNDEFINED)
                                {
                                  NSString *ext = [NSString stringWithUTF8String: (const char *)prop->data];
                                  if ([ext caseInsensitiveCompare: @".txt"] == NSOrderedSame)
                                    {
                                      type = @"text";
                                      subtype = @"plain";
                                    }
                                  else
                                    {
                                      [self warnWithFormat: @"Unidentified extension %@", ext];
                                    }
                                }
                            }

                          NSString *cid = partName;
                          prop = MAPIFindProperty(&(p->MAPI), PROP_TAG(PT_UNICODE, 0x3712)); // PR_CONTENT_IDENTIFIER?
                          if (prop != MAPI_UNDEFINED)
                            {
                              cid = [NSString stringWithUTF8String: (const char *)prop->data];
                            }

                          NSData *attachment;
                          if (NULL != filedata->data) {
                            if (isObject)
                              attachment = [NSData dataWithBytes: filedata->data + 16 length: filedata->size - 16];
                            else
                              attachment = [NSData dataWithBytes: filedata->data length: filedata->size];

                            [self bodyPartForAttachment: attachment
                                              withName: partName
                                                andType: type
                                            andSubtype: subtype
                                          andContentId: cid];
                          }

                          // count++;
                        }
                    } // if isRealAttachment
                } // if size>0
              p = p->next;
            }
        }
      TNEFFree(&tnef);
    }
}

- (void) setPart: (NGMimeBodyPart *) newPart
{
  ASSIGN (part, newPart);
  if (newPart)
    {
      NSMutableDictionary *info;

      info = [NSMutableDictionary dictionaryWithDictionary: [newPart bodyInfo]];
      if ([newPart encoding])
        [info setObject: [newPart encoding]
                 forKey: @"encoding"];
      [self setFilename: [[newPart bodyInfo] filename]];
      [self setPartInfo: info];
    }
  else
    {
      [self setFilename: nil];
      [self setPartInfo: nil];
    }
}

- (void) setFilename: (NSString *) newFilename
{
  ASSIGN (filename, newFilename);
}

- (void) setPartInfo: (id) newPartInfo
{
  ASSIGN (partInfo, newPartInfo);
}

- (NSString *) contentDispositionForAttachmentWithName: (NSString *) _name
                                               andSize: (NSNumber *) _size
                                        andContentType: (NSString *) _type
{
  NSString *cdtype, *cd;

  if (([_type caseInsensitiveCompare: @"image"]    == NSOrderedSame) ||
      ([_type caseInsensitiveCompare: @"message"]  == NSOrderedSame))
    cdtype = @"inline";
  else
    cdtype = @"attachment";

  cd = [NSString stringWithFormat: @"%@; filename=\"%@\"; size=%i;",
                 cdtype, [_name stringByReplacingOccurrencesOfString:@"\"" withString:@"\\\""], [_size intValue]];

  return cd;
}

- (NGMimeBodyPart *) bodyPartForData: (NSData *)   _data
                            withType: (NSString *) _type
                          andSubtype: (NSString *) _subtype
{
  NGMutableHashMap *map;
  NGMimeBodyPart   *bodyPart;
  NSData           *content;
  id                body;

  if (_data == nil) return nil;

  /* check attachment */

  /* prepare header of body part */

  map = [[[NGMutableHashMap alloc] initWithCapacity: 4] autorelease];

  // Content-Type
  [map setObject: [NSString stringWithFormat: @"%@/%@", _type, _subtype]
          forKey: @"content-type"];

  // Encoding
  [map setObject: @"utf-8"
          forKey: @"content-transfer-encoding"];

  /* prepare body content */

  content = [NSData dataWithBytes: [_data bytes] length: [_data length]];
  [map setObject: [NSNumber numberWithInt: [content length]]
          forKey: @"content-length"];

  /* Note: the -init method will create a temporary file! */
  body = [[[NGMimeFileData alloc] initWithBytes: [content bytes]
                                         length: [content length]] autorelease];

  bodyPart = [[[NGMimeBodyPart alloc] initWithHeader: map] autorelease];
  [bodyPart setBody: body];
  [bodyParts addBodyPart: bodyPart];

  return bodyPart;
}

- (NGMimeBodyPart *) bodyPartForAttachment: (NSData *)   _data
                                  withName: (NSString *) _name
                                   andType: (NSString *) _type
                                andSubtype: (NSString *) _subtype
                              andContentId: (NSString *) _cid
{
  NGMutableHashMap *map;
  NGMimeBodyPart   *bodyPart;
  NSData           *content;
  NSString         *s;
  id body;

  if (_name == nil) return nil;

  /* prepare header of body part */

  map = [[[NGMutableHashMap alloc] initWithCapacity: 4] autorelease];

  // Content-Type
  [map setObject: [NSString stringWithFormat: @"%@/%@", _type, _subtype]
          forKey: @"content-type"];

  // Content-Id
  [map setObject: _cid
          forKey: @"content-id"];

  // Content-Disposition
  s = [self contentDispositionForAttachmentWithName: _name
                                            andSize: [NSNumber numberWithLong: [_data length]]
                                     andContentType: _type];
  NGMimeContentDispositionHeaderField *o;
  o = [[NGMimeContentDispositionHeaderField alloc] initWithString: s];
  [map setObject: o forKey: @"content-disposition"];
  [o release];

  /* prepare body content */

  content = [NSData dataWithBytes: [_data bytes] length: [_data length]];
  [map setObject: [NSNumber numberWithInt: [content length]]
          forKey: @"content-length"];

  /* Note: the -init method will create a temporary file! */
  body = [[NGMimeFileData alloc] initWithBytes: [content bytes]
                                        length: [content length]];

  bodyPart = [[[NGMimeBodyPart alloc] initWithHeader: map] autorelease];
  [bodyPart setBody: body];

  [body release];
  [bodyParts addBodyPart: bodyPart];

  return bodyPart;
}

@end /* SOGoTNEFMailBodyPart */
