/* MAPIStoreMailVolatileMessage.m - this file is part of SOGo
 *
 * Copyright (C) 2011-2012 Inverse inc
 *
 * Author: Wolfgang Sourdeau <wsourdeau@inverse.ca>
 *
 * This file is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 3, or (at your option)
 * any later version.
 *
 * This file 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 General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; see the file COPYING.  If not, write to
 * the Free Software Foundation, Inc., 59 Temple Place - Suite 330,
 * Boston, MA 02111-1307, USA.
 */

/* TODO:
   - calendar invitations
   - merge some code in a common module with SOGoDraftObject
*/

#import <Foundation/NSArray.h>
#import <Foundation/NSData.h>
#import <Foundation/NSDictionary.h>
#import <Foundation/NSScanner.h>
#import <Foundation/NSString.h>
#import <Foundation/NSValue.h>
#import <NGExtensions/NGBase64Coding.h>
#import <NGExtensions/NGHashMap.h>
#import <NGExtensions/NSObject+Logs.h>
#import <NGExtensions/NSObject+Values.h>
#import <NGExtensions/NSString+Encoding.h>
#import <NGMime/NGMimeBodyPart.h>
#import <NGMime/NGMimeMultipartBody.h>
#import <NGMail/NGMimeMessage.h>
#import <NGMail/NGMimeMessageGenerator.h>
#import <NGImap4/NGImap4Client.h>
#import <NGImap4/NGImap4Connection.h>
#import <SOGo/NSArray+Utilities.h>
#import <SOGo/NSCalendarDate+SOGo.h>
#import <SOGo/NSString+Utilities.h>
#import <SOGo/SOGoCacheObject.h>
#import <SOGo/SOGoDomainDefaults.h>
#import <SOGo/SOGoMailer.h>
#import <SOGo/SOGoUser.h>
#import <SOGo/SOGoUserManager.h>
#import <Mailer/SOGoMailFolder.h>
#import <Mailer/NSString+Mail.h>

#import "Codepages.h"
#import "MAPIStoreAttachment.h"
#import "MAPIStoreAttachmentTable.h"
#import "MAPIStoreContext.h"
#import "MAPIStoreMailFolder.h"
#import "MAPIStoreMIME.h"
#import "MAPIStoreMapping.h"
#import "MAPIStoreSamDBUtils.h"
#import "MAPIStoreTypes.h"
#import "MAPIStoreUserContext.h"
#import "NSData+MAPIStore.h"
#import "NSObject+MAPIStore.h"
#import "NSString+MAPIStore.h"

#import "MAPIStoreMailVolatileMessage.h"

#undef DEBUG
#include <mapistore/mapistore.h>
#include <mapistore/mapistore_errors.h>
#include <mapistore/mapistore_nameid.h>

static Class NSNumberK = Nil;

static NSString *recTypes[] = { @"orig", @"to", @"cc", @"bcc" };

//
// Useful extension that comes from Pantomime which is also
// released under the LGPL. We should eventually merge
// this with the same category found in SOPE's NGSmtpClient.m
// or simply drop sope-mime in favor of Pantomime
//
@interface NSMutableData (DataCleanupExtension)

- (NSRange) rangeOfCString: (const char *) theCString;
- (NSRange) rangeOfCString: (const char *) theCString
		  options: (unsigned int) theOptions
		    range: (NSRange) theRange;
@end

@implementation NSMutableData (DataCleanupExtension)

- (NSRange) rangeOfCString: (const char *) theCString
{
  return [self rangeOfCString: theCString
	       options: 0
	       range: NSMakeRange(0,[self length])];
}

-(NSRange) rangeOfCString: (const char *) theCString
		  options: (unsigned int) theOptions
		    range: (NSRange) theRange
{
  const char *b, *bytes;
  int i, len, slen;
  
  if (!theCString)
    {
      return NSMakeRange(NSNotFound,0);
    }
  
  bytes = [self bytes];
  len = [self length];
  slen = strlen(theCString);
  
  b = bytes;
  
  if (len > theRange.location + theRange.length)
    {
      len = theRange.location + theRange.length;
    }

  if (theOptions == NSCaseInsensitiveSearch)
    {
      i = theRange.location;
      b += i;
      
      for (; i <= len-slen; i++, b++)
	{
	  if (!strncasecmp(theCString,b,slen))
	    {
	      return NSMakeRange(i,slen);
	    }
	}
    }
  else
    {
      i = theRange.location;
      b += i;
      
      for (; i <= len-slen; i++, b++)
	{
	  if (!memcmp(theCString,b,slen))
	    {
	      return NSMakeRange(i,slen);
	    }
	}
    }
  
  return NSMakeRange(NSNotFound,0);
}

@end

@interface MAPIStoreAttachment (MAPIStoreMIME)

- (BOOL) hasContentId;
- (NGMimeBodyPart *) asMIMEBodyPart;

@end

@implementation MAPIStoreAttachment (MAPIStoreMIME)

- (BOOL) hasContentId
{
  NSString *contentId = [properties
                          objectForKey: MAPIPropertyKey (PR_ATTACH_CONTENT_ID_UNICODE)];
  return contentId && [contentId length] > 0;
}

- (NGMimeBodyPart *) asMIMEBodyPart
{
  NGMimeBodyPart *bodyPart = nil;
  NSString *filename, *mimeType, *baseDisposition, *contentType,
    *contentDisposition, *contentId;
  NSData *content;
  struct mapistore_connection_info *connInfo;
  SOGoDomainDefaults *dd;
  SOGoUser *activeUser;
  NGMutableHashMap *map;

  content = [properties objectForKey: MAPIPropertyKey (PR_ATTACH_DATA_BIN)];
  if (content)
    {
      filename
        = [properties
            objectForKey: MAPIPropertyKey (PR_ATTACH_LONG_FILENAME_UNICODE)];
      if (![filename length])
        filename
          = [properties
            objectForKey: MAPIPropertyKey (PR_ATTACH_FILENAME_UNICODE)];
      
      mimeType = [properties
                   objectForKey: MAPIPropertyKey (PR_ATTACH_MIME_TAG_UNICODE)];
      if (!mimeType && [filename length])
        mimeType = [[MAPIStoreMIME sharedMAPIStoreMIME]
                     mimeTypeForExtension: [filename pathExtension]];
      if (!mimeType)
        mimeType = @"application/octet-stream";
      
      if ([mimeType hasPrefix: @"text/"])
        {
          connInfo = [[self context] connectionInfo];
          activeUser = [SOGoUser userWithLogin: [NSString stringWithUTF8String: connInfo->username]];
          dd = [activeUser domainDefaults];
          baseDisposition = ([dd mailAttachTextDocumentsInline]
                             ? @"inline" : @"attachment");
        }
      else if ([mimeType hasPrefix: @"image/"] || [mimeType hasPrefix: @"message"])
        baseDisposition = @"inline";
      else
        baseDisposition = @"attachment";
      
      if ([filename length] > 0)
        {
          contentType = [NSString stringWithFormat: @"%@; name=\"%@\"",
                                  mimeType, filename];
          contentDisposition = [NSString stringWithFormat: @"%@; filename=\"%@\"",
                                         baseDisposition, filename];
        }
      else
        {
          contentType = mimeType;
          contentDisposition = baseDisposition;
        }
      
      map = [[NGMutableHashMap alloc] initWithCapacity: 16];
      [map addObject: contentType forKey: @"content-type"];
      [map addObject: contentDisposition forKey: @"content-disposition"];
      contentId = [properties
                     objectForKey: MAPIPropertyKey (PR_ATTACH_CONTENT_ID_UNICODE)];
      if (contentId && [contentId length] > 0)
        [map setObject: [NSString stringWithFormat: @"<%@>", contentId]
                forKey: @"content-id"];
      bodyPart = [NGMimeBodyPart bodyPartWithHeader: map];
      [bodyPart setBody: content];
      [map release];
    }
  else
    [self errorWithFormat: @"no content for attachment"];

  return bodyPart;
}

@end

@implementation MAPIStoreMailVolatileMessage

+ (void) initialize
{
  NSNumberK = [NSNumber class];
}

- (id) initWithSOGoObject: (id) newSOGoObject
              inContainer: (MAPIStoreObject *) newContainer
{
  if ((self = [super initWithSOGoObject: newSOGoObject
                            inContainer: newContainer]))
    {
      ASSIGN (properties, [sogoObject properties]);
    }

  return self;
}

- (void) addProperties: (NSDictionary *) newProperties
{
  [super addProperties: newProperties];
  [sogoObject adjustLastModified];
}

- (BOOL) canGetProperty: (enum MAPITAGS) propTag
{
  return ([super canGetProperty: propTag]
          || [properties objectForKey: MAPIPropertyKey (propTag)] != nil);
}

- (uint64_t) objectVersion
{
  NSNumber *version;

  version = [properties objectForKey: @"version"];

  return (version
          ? [version unsignedLongLongValue]
          : ULLONG_MAX);
}

- (enum mapistore_error) getPidTagMessageClass: (void **) data inMemCtx: (TALLOC_CTX *) memCtx
{
  *data = [@"IPM.Note" asUnicodeInMemCtx: memCtx];

  return MAPISTORE_SUCCESS;
}

- (enum mapistore_error) getPidTagChangeKey: (void **) data inMemCtx: (TALLOC_CTX *) memCtx
{
  NSData *changeKey;
  enum mapistore_error rc;

  changeKey = [properties objectForKey: MAPIPropertyKey (PR_CHANGE_KEY)];
  if (changeKey)
    {
      *data = [changeKey asBinaryInMemCtx: memCtx];
      rc = MAPISTORE_SUCCESS;
    }
  else
    rc = [super getPidTagChangeKey: data inMemCtx: memCtx];

  return rc;
}

- (NSDate *) creationTime
{
  return [sogoObject creationDate];
}

- (NSDate *) lastModificationTime
{
  return [sogoObject lastModified];
}

- (id) lookupAttachment: (NSString *) childKey
{
  return [attachmentParts objectForKey: childKey];
}

- (void) getMessageData: (struct mapistore_message **) dataPtr
               inMemCtx: (TALLOC_CTX *) memCtx
{
  NSArray *recipients;
  NSUInteger count, max, recipientsMax, p, current;
  NSString *username, *cn, *email;
  NSData *entryId;
  NSDictionary *allRecipients, *dict, *contactInfos;
  SOGoUserManager *mgr;
  struct mapistore_message *msgData;
  struct mapistore_message_recipient *recipient;
  enum ulRecipClass type;

  // [super getMessageData: &msgData inMemCtx: memCtx];

  msgData = talloc_zero (memCtx, struct mapistore_message);

  allRecipients = [properties objectForKey: @"recipients"];
  msgData->columns = set_SPropTagArray (msgData, 9,
                                        PR_OBJECT_TYPE,
                                        PR_DISPLAY_TYPE,
                                        PR_7BIT_DISPLAY_NAME_UNICODE,
                                        PR_SMTP_ADDRESS_UNICODE,
                                        PR_SEND_INTERNET_ENCODING,
                                        PR_RECIPIENT_DISPLAY_NAME_UNICODE,
                                        PR_RECIPIENT_FLAGS,
                                        PR_RECIPIENT_ENTRYID,
                                        PR_RECIPIENT_TRACKSTATUS);
  
  /* Retrieve recipients from the message */
  max = ([[allRecipients objectForKey: @"orig"] count]
         + [[allRecipients objectForKey: @"to"] count]
         + [[allRecipients objectForKey: @"cc"] count]
         + [[allRecipients objectForKey: @"bcc"] count]);

  mgr = [SOGoUserManager sharedUserManager];
  msgData->recipients_count = max;
  msgData->recipients = talloc_array (msgData, struct mapistore_message_recipient, max);
  current = 0;
  for (type = MAPI_ORIG; type <= MAPI_BCC; type++)
    {
      recipients = [allRecipients objectForKey: recTypes[type]];
      recipientsMax = [recipients count];
      for (count = 0; count < recipientsMax; count++)
        {
          recipient = msgData->recipients + current;
          recipient->type = type;

          dict = [recipients objectAtIndex: count];
          cn = [dict objectForKey: @"fullName"];
          email = [dict objectForKey: @"email"];

          contactInfos = [mgr contactInfosForUserWithUIDorEmail: email];
          if (contactInfos)
            {
              username = [contactInfos objectForKey: @"sAMAccountName"];
              recipient->username = [username asUnicodeInMemCtx: msgData];
              entryId = MAPIStoreInternalEntryId ([[self context] connectionInfo], username);
            }
          else
            {
              recipient->username = NULL;
              entryId = MAPIStoreExternalEntryId (cn, email);
            }

          /* properties */
          p = 0;
          recipient->data = talloc_array (msgData, void *, msgData->columns->cValues);
          memset (recipient->data, 0, msgData->columns->cValues * sizeof (void *));

          // PR_OBJECT_TYPE = MAPI_MAILUSER (see MAPI_OBJTYPE)
          recipient->data[p] = MAPILongValue (msgData, MAPI_MAILUSER);
          p++;

          // PR_DISPLAY_TYPE = DT_MAILUSER (see MS-NSPI)
          recipient->data[p] = MAPILongValue (msgData, 0);
          p++;

          // PR_7BIT_DISPLAY_NAME_UNICODE
          recipient->data[p] = [cn asUnicodeInMemCtx: msgData];
          p++;

          // PR_SMTP_ADDRESS_UNICODE
          recipient->data[p] = [email asUnicodeInMemCtx: msgData];
          p++;

          // PR_SEND_INTERNET_ENCODING = 0x00060000 (plain text, see OXCMAIL)
          recipient->data[p] = MAPILongValue (msgData, 0x00060000);
          p++;

          // PR_RECIPIENT_DISPLAY_NAME_UNICODE
          recipient->data[p] = [cn asUnicodeInMemCtx: msgData];
          p++;

          // PR_RECIPIENT_FLAGS
          recipient->data[p] = MAPILongValue (msgData, 0x01);
          p++;

          // PR_RECIPIENT_ENTRYID
          recipient->data[p] = [entryId asBinaryInMemCtx: msgData];
          p++;

          // PR_RECIPIENT_TRACKSTATUS
          recipient->data[p] = MAPILongValue (msgData, 0x00);
          p++;

          current++;
        }
    }
  *dataPtr = msgData;
}

static inline NSString *
MakeRecipientString (NSDictionary *recipient)
{
  NSString *fullName, *email, *fullEmail;

  fullName = [recipient objectForKey: @"fullName"];
  email = [recipient objectForKey: @"email"];
  if ([email length] > 0)
    {
      if ([fullName length] > 0)
        fullEmail = [NSString stringWithFormat: @"%@ <%@>", fullName, email];
      else
        fullEmail = email;
    }
  else
    {
      NSLog (@"recipient not generated from record: %@", recipient);
      fullEmail = nil;
    }

  return fullEmail;
}

static inline NSArray *
MakeRecipientsList (NSArray *recipients)
{
  NSMutableArray *list;
  NSUInteger count, max;
  NSString *recipient;

  max = [recipients count];
  list = [NSMutableArray arrayWithCapacity: max];
  for (count = 0; count < max; count++)
    {
      recipient = MakeRecipientString ([recipients objectAtIndex: count]);
      if (recipient)
        [list addObject: recipient];
    }

  return list;
}

static NSString *
QuoteSpecials (NSString *address)
{
  NSString *result, *part, *s2;
  int i, len;

  // We want to correctly send mails to recipients such as :
  // foo.bar
  // foo (bar) <foo@zot.com>
  // bar, foo <foo@zot.com>
  if ([address indexOf: '('] >= 0 || [address indexOf: ')'] >= 0
      || [address indexOf: '<'] >= 0 || [address indexOf: '>'] >= 0
      || [address indexOf: '@'] >= 0 || [address indexOf: ','] >= 0
      || [address indexOf: ';'] >= 0 || [address indexOf: ':'] >= 0
      || [address indexOf: '\\'] >= 0 || [address indexOf: '"'] >= 0
      || [address indexOf: '.'] >= 0
      || [address indexOf: '['] >= 0 || [address indexOf: ']'] >= 0)
    {
      // We search for the first instance of < from the end
      // and we quote what was before if we need to
      len = [address length];
      i = -1;
      while (len--)
        if ([address characterAtIndex: len] == '<')
          {
            i = len;
            break;
          }

      if (i > 0)
        {
          part = [address substringToIndex: i - 1];
          s2 = [[part stringByReplacingString: @"\\" withString: @"\\\\"]
                     stringByReplacingString: @"\"" withString: @"\\\""];
          result = [NSString stringWithFormat: @"\"%@\" %@", s2, [address substringFromIndex: i]];
        }
      else
        {
          s2 = [[address stringByReplacingString: @"\\" withString: @"\\\\"]
                     stringByReplacingString: @"\"" withString: @"\\\""];
          result = [NSString stringWithFormat: @"\"%@\"", s2];
        }
    }
  else
    result = address;

  return result;
}

static inline void
FillMessageHeadersFromSharingProperties (NGMutableHashMap *headers, NSDictionary *mailProperties)
{
  /* Store the *important* properties related with a sharing object as
     MIME eXtension headers. See [MS-OXSHARE] Section 2.2 for details
     about the properties */

  id value;
  NSNumber *sharingFlavourNum = nil;

  value = [mailProperties objectForKey: MAPIPropertyKey (PidLidSharingCapabilities)];
  if (value)
    [headers setObject: value
                forKey: @"X-MS-Sharing-Capabilities"];

  value = [mailProperties objectForKey: MAPIPropertyKey (PidLidSharingFlavor)];
  if (value)
    sharingFlavourNum = (NSNumber *)value;
  else
    {
      value = [mailProperties objectForKey: MAPIPropertyKey (PidNameXSharingFlavor)];
      if (value)
        {
          /* Transform the hex string to unsigned int */
          NSScanner *scanner;
          unsigned int sharingFlavour;
          scanner = [NSScanner scannerWithString:value];
          if ([scanner scanHexInt:&sharingFlavour])
            sharingFlavourNum =[NSNumber numberWithUnsignedInt: sharingFlavour];
        }
    }
  if (sharingFlavourNum)
    {
      if ([sharingFlavourNum unsignedIntegerValue] == 0x5100)
        {
          /* 0x5100 sharing flavour is not in standard but it seems to
             be a denial of request + invitation message so we store
             deny sharing flavour */
          sharingFlavourNum = [NSNumber numberWithUnsignedInt: SHARING_DENY_REQUEST];
        }
      [headers setObject: sharingFlavourNum
                  forKey: @"X-MS-Sharing-Flavor"];
    }

  value = [mailProperties objectForKey: MAPIPropertyKey (PidLidSharingInitiatorEntryId)];
  if (value)
    [headers setObject: [[value stringByEncodingBase64] stringByReplacingOccurrencesOfString: @"\n"
                                                                                  withString: @""]
                forKey: @"X-MS-Sharing-InitiatorEntryId"];

  value = [mailProperties objectForKey: MAPIPropertyKey (PidLidSharingInitiatorName)];
  if (value)
    [headers setObject: value
                forKey: @"X-MS-Sharing-InitiatorName"];

  value = [mailProperties objectForKey: MAPIPropertyKey (PidLidSharingInitiatorSmtp)];
  if (value)
    [headers setObject: value
                forKey: @"X-MS-Sharing-InitiatorSmtp"];

  value = [mailProperties objectForKey: MAPIPropertyKey (PidLidSharingLocalType)];
  if (value)
    [headers setObject: value
                forKey: @"X-MS-Sharing-LocalType"];

  value = [mailProperties objectForKey: MAPIPropertyKey (PidLidSharingProviderName)];
  if (value)
    [headers setObject: value
                forKey: @"X-MS-Sharing-ProviderName"];

  value = [mailProperties objectForKey: MAPIPropertyKey (PidLidSharingRemoteName)];
  if (value)
    [headers setObject: value
                forKey: @"X-MS-Sharing-RemoteName"];

  value = [mailProperties objectForKey: MAPIPropertyKey (PidLidSharingRemoteStoreUid)];
  if (value)
    [headers setObject: value
                forKey: @"X-MS-Sharing-RemoteStoreUid"];

  value = [mailProperties objectForKey: MAPIPropertyKey (PidLidSharingRemoteUid)];
  if (value)
    [headers setObject: value
                forKey: @"X-MS-Sharing-RemoteUid"];

  value = [mailProperties objectForKey: MAPIPropertyKey (PidLidSharingResponseTime)];
  if (value)
    [headers setObject: value
                forKey: @"X-MS-Sharing-ResponseTime"];

  value = [mailProperties objectForKey: MAPIPropertyKey (PidLidSharingResponseType)];
  if (value)
    [headers setObject: value
                forKey: @"X-MS-Sharing-ResponseType"];

}

static inline void
FillMessageHeadersFromProperties (NGMutableHashMap *headers,
                                  NSDictionary *mailProperties, BOOL withBcc,
                                  struct mapistore_connection_info *connInfo)
{
  BOOL fromResolved = NO;
  NSData *senderEntryId;
  NSMutableString *subject;
  NSString *from, *recId, *messageId, *subjectData, *recipientsStr, *msgClass;
  NSArray *list;
  NSCalendarDate *date;
  NSDictionary *recipients;
  enum ulRecipClass type, bccLimit;
  SOGoUser *activeUser;
  NSNumber *priority;

  activeUser
    = [SOGoUser
        userWithLogin: [NSString stringWithUTF8String: connInfo->username]];

  from = [NSString stringWithFormat: @"%@ <%@>",
                   [activeUser cn], [[activeUser allEmails] objectAtIndex: 0]];
  [headers setObject: QuoteSpecials (from) forKey: @"from"];

  /* save the recipients */
  recipients = [mailProperties objectForKey: @"recipients"];
  if (recipients)
    {
      if (withBcc)
        bccLimit = MAPI_BCC;
      else
        bccLimit = MAPI_CC;

      for (type = MAPI_TO; type <= bccLimit; type++)
	{
	  recId = recTypes[type];
	  list = MakeRecipientsList ([recipients objectForKey: recId]);
          [headers setObjects: list forKey: recId];
	}

      list = MakeRecipientsList ([recipients objectForKey: @"orig"]);
      if ([list count])
        {
          [headers setObjects: list forKey: @"from"];
          fromResolved = YES;
        }
    }

  if (!fromResolved)
    {
      TALLOC_CTX *local_mem_ctx;
      local_mem_ctx = talloc_new(NULL);
      if (!local_mem_ctx)
        {
          NSLog (@"%s: Out of memory", __PRETTY_FUNCTION__);
          return;
        }

      NSLog (@"Message without an orig from, try to guess it from PidTagSenderEntryId");
      senderEntryId = [mailProperties objectForKey: MAPIPropertyKey (PR_SENDER_ENTRYID)];
      if (senderEntryId)
        {
          struct Binary_r bin32;
          struct AddressBookEntryId *addrBookEntryId;
          NSString *username;
          NSMutableDictionary *fromRecipient;

          fromRecipient = [NSMutableDictionary dictionaryWithCapacity: 2];

          bin32.cb = [senderEntryId length];
          bin32.lpb = (uint8_t *) [senderEntryId bytes];
          addrBookEntryId = get_AddressBookEntryId (local_mem_ctx, &bin32);
          if (addrBookEntryId && [[NSString stringWithGUID: &addrBookEntryId->ProviderUID]
                                   hasSuffix: @"08002b2fe182"])
            {
              /* TODO: better way to distinguish local and other ones */
              username = MAPIStoreSamDBUserAttribute (connInfo, @"legacyExchangeDN",
                                                      [NSString stringWithUTF8String: addrBookEntryId->X500DN], @"sAMAccountName");
              if (username)
                {
                  SOGoUser *fromUser;

                  fromUser = [SOGoUser userWithLogin: [username lowercaseString]];
                  [fromRecipient setObject: [fromUser cn] forKey: @"fullName"];
                  [fromRecipient setObject: [[fromUser allEmails] objectAtIndex: 0]
                                    forKey: @"email"];
                }
              else
                  [fromRecipient setObject: [NSString stringWithUTF8String: addrBookEntryId->X500DN]
                                    forKey: @"email"];

            }
          else
            {
              /* Try with One-Off EntryId */
              struct OneOffEntryId *oneOffEntryId;

              oneOffEntryId = get_OneOffEntryId (local_mem_ctx, &bin32);
              if (oneOffEntryId && [[NSString stringWithGUID: &oneOffEntryId->ProviderUID]
                                     hasSuffix: @"00dd010f5402"])
                {
                  [fromRecipient setObject: [NSString stringWithUTF8String: oneOffEntryId->DisplayName.lpszW]
                                    forKey: @"fullName"];
                  [fromRecipient setObject: [NSString stringWithUTF8String: oneOffEntryId->EmailAddress.lpszW]
                                    forKey: @"email"];
                }
            }

          if ([[fromRecipient allKeys] count] > 0)
            {
              list = MakeRecipientsList ([NSArray arrayWithObjects: fromRecipient, nil]);
              if ([list count])
                [headers setObjects: list forKey: @"from"];
            }

        }
      /* Free entryId */
      talloc_free(local_mem_ctx);
    }

  if (!recipients)
    {
      NSLog (@"Message without recipients."
             @"Guessing recipients from PidTagOriginalDisplayTo and PidTagOriginalCc");
      recipientsStr = [mailProperties objectForKey: MAPIPropertyKey (PidTagOriginalDisplayTo)];
      if (recipientsStr)
        {
          list = [recipientsStr componentsSeparatedByString:@", "];
          if ([list count])
            [headers setObjects: list forKey: @"to"];
        }

      recipientsStr = [mailProperties objectForKey: MAPIPropertyKey (PidTagOriginalDisplayCc)];
      if (recipientsStr)
        {
          list = [recipientsStr componentsSeparatedByString:@", "];
          if ([list count])
            [headers setObjects: list forKey: @"cc"];
        }
    }

  subject = [NSMutableString stringWithCapacity: 128];
  subjectData = [mailProperties objectForKey: MAPIPropertyKey (PR_SUBJECT_PREFIX_UNICODE)];
  if (subjectData)
    [subject appendString: subjectData];
  subjectData = [mailProperties objectForKey: MAPIPropertyKey (PR_NORMALIZED_SUBJECT_UNICODE)];
  if (subjectData)
    [subject appendString: subjectData];
  if ([subject length] == 0)
    {
      subjectData = [mailProperties objectForKey: MAPIPropertyKey (PR_SUBJECT_UNICODE)];
      if (subjectData)
        [subject appendString: subjectData];
    }
  [headers setObject: [subject asQPSubjectString: @"utf-8"] forKey: @"subject"];

  messageId = [mailProperties objectForKey: MAPIPropertyKey (PR_INTERNET_MESSAGE_ID_UNICODE)];
  if ([messageId length])
    [headers setObject: messageId forKey: @"message-id"];

  date = [mailProperties objectForKey: MAPIPropertyKey (PR_CLIENT_SUBMIT_TIME)];
  if (date)
    {
      [headers addObject: [date rfc822DateString] forKey: @"date"];
    }
  [headers addObject: @"1.0" forKey: @"MIME-Version"];

  priority = [mailProperties objectForKey: MAPIPropertyKey (PidTagImportance)];
  
  if ([priority intValue] == 2)
    {
      [headers addObject: @"1 (Highest)"  forKey: @"X-Priority"];
    }
  else if ([priority intValue] == 1)
    {
      [headers removeAllObjectsForKey: @"X-Priority"];
    }
  else
    {
      [headers addObject: @"5 (Lowest)"  forKey: @"X-Priority"];
    }

  msgClass = [mailProperties objectForKey: MAPIPropertyKey (PidTagMessageClass)];
  if ([msgClass isEqualToString: @"IPM.Sharing"])
    {
      FillMessageHeadersFromSharingProperties (headers, mailProperties);
    }

}

static NSArray *
MakeAttachmentParts (NSDictionary *attachmentParts, BOOL withContentId)
{
  NSMutableArray *attachmentMimeParts;
  NSArray *keys;
  MAPIStoreAttachment *attachment;
  NSUInteger count, max;
  NGMimeBodyPart *mimePart;

  keys = [attachmentParts allKeys];
  max = [keys count];
  attachmentMimeParts = [NSMutableArray arrayWithCapacity: max];
  for (count = 0; count < max; count++)
    {
      attachment = [attachmentParts
                     objectForKey: [keys objectAtIndex: count]];
      if ([attachment hasContentId] == withContentId)
        {
          mimePart = [attachment asMIMEBodyPart];
          if (mimePart)
            [attachmentMimeParts addObject: mimePart];
        }
    }

  return attachmentMimeParts;
}

static inline id
MakeTextPlainBody (NSDictionary *mailProperties, NSString **contentType)
{
  id textPlainBody;

  textPlainBody = [[mailProperties
                      objectForKey: MAPIPropertyKey (PR_BODY_UNICODE)]
                    dataUsingEncoding: NSUTF8StringEncoding];
  *contentType = @"text/plain; charset=utf-8";

  return textPlainBody;
}

static inline id
MakeTextHtmlBody (NSDictionary *mailProperties, NSDictionary *attachmentParts,
                  NSString **contentType)
{
  id textHtmlBody;
  NSData *htmlBody;
  NSString *charset, *htmlContentType;
  NSArray *parts;
  NSNumber *codePage;
  NGMimeBodyPart *htmlBodyPart;
  NGMutableHashMap *headers;
  NSUInteger count, max;

  htmlBody = [mailProperties objectForKey: MAPIPropertyKey (PR_HTML)];
  if (htmlBody)
    {
      /* charset */
      codePage = [mailProperties objectForKey: MAPIPropertyKey (PR_INTERNET_CPID)];
      charset = [Codepages getNameFromCodepage: codePage];
      if (!charset)
        charset = @"utf-8";
      htmlContentType = [NSString stringWithFormat: @"text/html; charset=%@",
                                  charset];

      parts = MakeAttachmentParts (attachmentParts, YES);
      max = [parts count];
      if (max > 0)
        {
          textHtmlBody = [NGMimeMultipartBody new];
          [textHtmlBody autorelease];

          headers = [[NGMutableHashMap alloc] initWithCapacity: 1];
          [headers setObject: htmlContentType forKey: @"content-type"];
          htmlBodyPart = [NGMimeBodyPart bodyPartWithHeader: headers];
          [htmlBodyPart setBody: htmlBody];
          [headers release];
          [textHtmlBody addBodyPart: htmlBodyPart];

          for (count = 0; count < max; count++)
            [textHtmlBody addBodyPart: [parts objectAtIndex: count]];

          *contentType = @"multipart/related";
        }
      else
        {
          textHtmlBody = htmlBody;
          *contentType = htmlContentType;
        }
    }
  else
    textHtmlBody = nil;

  return textHtmlBody;
}

static inline id
MakeTextPartBody (NSDictionary *mailProperties, NSDictionary *attachmentParts,
                  NSString **contentType)
{
  id textBody, textPlainBody, textHtmlBody;
  NSString *textPlainContentType, *textHtmlContentType;
  NGMutableHashMap *headers;
  NGMimeBodyPart *bodyPart;

  textPlainBody = MakeTextPlainBody (mailProperties, &textPlainContentType);
  textHtmlBody = MakeTextHtmlBody (mailProperties, attachmentParts, &textHtmlContentType);
  if (textPlainBody)
    {
      if (textHtmlBody)
        {
          textBody = [NGMimeMultipartBody new];
          [textBody autorelease];

          headers = [[NGMutableHashMap alloc] initWithCapacity: 1];
          [headers setObject: textHtmlContentType forKey: @"content-type"];
          bodyPart = [NGMimeBodyPart bodyPartWithHeader: headers];
          [bodyPart setBody: textHtmlBody];
          [headers release];
          [textBody addBodyPart: bodyPart];

          headers = [[NGMutableHashMap alloc] initWithCapacity: 1];
          [headers setObject: textPlainContentType forKey: @"content-type"];
          bodyPart = [NGMimeBodyPart bodyPartWithHeader: headers];
          [bodyPart setBody: textPlainBody];
          [headers release];
          [textBody addBodyPart: bodyPart];

          *contentType = @"multipart/alternative";
        }
      else
        {
          textBody = textPlainBody;
          *contentType = textPlainContentType;
        }
    }
  else
    {
      textBody = textHtmlBody;
      *contentType = textHtmlContentType;
    }

  return textBody;
}

// static id
// MakeMessageBody (NSDictionary *mailProperties, NSDictionary *attachmentParts,
//                  NSString **contentType)
static id
MakeMessageBody (NSDictionary *mailProperties, NSDictionary *attachmentParts, NSString **contentType)
{
  id messageBody, textBody;
  NSString *textContentType;
  NSArray *parts;
  NGMimeBodyPart *textBodyPart;
  NGMutableHashMap *headers;
  NSUInteger count, max;

  textBody = MakeTextPartBody (mailProperties, attachmentParts,
                               &textContentType);

  parts = MakeAttachmentParts (attachmentParts, NO);
  max = [parts count];
  if (max > 0)
    {
      messageBody = [NGMimeMultipartBody new];
      [messageBody autorelease];

      if (textBody)
        {
          headers = [[NGMutableHashMap alloc] initWithCapacity: 1];
          [headers setObject: textContentType forKey: @"content-type"];
          textBodyPart = [NGMimeBodyPart bodyPartWithHeader: headers];
          [textBodyPart setBody: textBody];
          [headers release];
          [messageBody addBodyPart: textBodyPart];
        }

      for (count = 0; count < max; count++)
        [messageBody addBodyPart: [parts objectAtIndex: count]];

      *contentType = @"multipart/mixed";
    }
  else
    {
      messageBody = textBody;
      *contentType = textContentType;
    }

  return messageBody;
}

- (NGMimeMessage *) _generateMessageWithBcc: (BOOL) withBcc
{
  NSString *contentType;
  NGMimeMessage *message;
  NGMutableHashMap *headers;
  id messageBody;

  headers = [[NGMutableHashMap alloc] initWithCapacity: 16];
  FillMessageHeadersFromProperties (headers, properties, withBcc,
                                    [[self context] connectionInfo]);
  message = [[NGMimeMessage alloc] initWithHeader: headers];
  [message autorelease];
  [headers release];

  messageBody = MakeMessageBody (properties, attachmentParts, &contentType);
  if (messageBody)
    {
      [message setHeader: contentType forKey: @"content-type"];
      [message setBody: messageBody];
    }

  return message;
}

- (NSData *) _generateMailDataWithBcc: (BOOL) withBcc
{
  NGMimeMessage *message;
  NGMimeMessageGenerator *generator;
  NSData *messageData;

  /* mime message generation */
  generator = [NGMimeMessageGenerator new];
  message = [self _generateMessageWithBcc: withBcc];
  messageData = [generator generateMimeFromPart: message];
  [generator release];

  // [messageData writeToFile: @"/tmp/mimegen.eml" atomically: NO];

  return messageData;
}

- (enum mapistore_error) submitWithFlags: (enum SubmitFlags) flags
{
  enum mapistore_error rc = MAPISTORE_SUCCESS;
  NSDictionary *recipients;
  NSData *messageData;
  NSMutableArray *recipientEmails;
  NSArray *list;
  NSString *recId, *from, *msgClass;
  enum ulRecipClass type;
  SOGoUser *activeUser;
  SOGoDomainDefaults *dd;
  NSException *error;
  WOContext *woContext;
  id <SOGoAuthenticator> authenticator;

  msgClass = [properties objectForKey: MAPIPropertyKey (PidTagMessageClass)];
  if ([msgClass isEqualToString: @"IPM.Note"] || [msgClass isEqualToString: @"IPM.Sharing"]) /* we skip invitation replies */
    {
      /* send mail */

      messageData = [self _generateMailDataWithBcc: NO];

      recipientEmails = [NSMutableArray arrayWithCapacity: 32];
      recipients = [properties objectForKey: @"recipients"];
      for (type = MAPI_ORIG; type <= MAPI_BCC; type++)
        {
          recId = recTypes[type];
          list = [recipients objectForKey: recId];
          [recipientEmails
            addObjectsFromArray: [list objectsForKey: @"email"
                                      notFoundMarker: nil]];
        }

      activeUser = [[self context] activeUser];

      [self logWithFormat: @"recipients: %@", recipientEmails];
      dd = [activeUser domainDefaults];
      from = [[activeUser allEmails] objectAtIndex: 0];

      [[self userContext] activate];
      woContext = [[self userContext] woContext];
      authenticator = [sogoObject authenticatorInContext: woContext];
      error = [[SOGoMailer mailerWithDomainDefaults: dd]
                       sendMailData: messageData
                       toRecipients: recipientEmails
                             sender: from
                  withAuthenticator: authenticator
                          inContext: woContext];
      if (error)
        {
          [self errorWithFormat: @"an error occurred: '%@'", error];
          rc = MAPISTORE_ERR_MSG_SEND;
        }

      // mapping = [self mapping];
      // [mapping unregisterURLWithID: [self objectId]];
      // [self setIsNew: NO];
      // [properties removeAllObjects];
      [(MAPIStoreMailFolder *) [self container] cleanupCaches];
    }
  else
    [self logWithFormat: @"skipping submit of message with class '%@'",
          msgClass];

  return rc;
}

- (void) save: (TALLOC_CTX *) memCtx
{
  BOOL updatedMetadata;
  NSString *folderName, *flag, *newIdString, *messageKey, *changeNumber;
  NSData *changeKey, *messageData;
  NGImap4Connection *connection;
  NGImap4Client *client;
  SOGoMailFolder *containerFolder;
  NSDictionary *result, *responseResult;
  MAPIStoreMapping *mapping;
  uint64_t mid;

  messageData = [self _generateMailDataWithBcc: YES];

  /* appending to imap folder */
  containerFolder = [container sogoObject];
  connection = [containerFolder imap4Connection];
  client = [connection client];
  folderName = [connection imap4FolderNameForURL: [containerFolder imap4URL]];
  result = [client append: messageData toFolder: folderName
                withFlags: [NSArray arrayWithObjects: @"seen", nil]];
  if ([[result objectForKey: @"result"] boolValue])
    {
      /* we reregister the new message URL with the id mapper */
      responseResult = [[result objectForKey: @"RawResponse"]
                         objectForKey: @"ResponseResult"];
      flag = [responseResult objectForKey: @"flag"];

      newIdString = [[flag componentsSeparatedByString: @" "]
                      objectAtIndex: 2];
      mapping = [self mapping];
      mid = [self objectId];
      [mapping unregisterURLWithID: mid];
      // [sogoObject setNameInContainer: ];

      messageKey = [NSString stringWithFormat: @"%@.eml", newIdString];
      [sogoObject setNameInContainer: messageKey];
      [mapping registerURL: [self url] withID: mid];

      /* synchronise the cache and update the predecessor change list
         with the change key provided by the client. Before doing
         this, lets issue a unselect/select combo because of timing
         issues with Dovecot in obtaining the latest modseq.
         Sometimes, Dovecot doesn't return the newly appended UID if
         we do a "UID SORT (DATE) UTF-8 (MODSEQ XYZ) (NOT DELETED)"
         command (where XYZ is the HIGHESTMODSEQ+1) immediately after
         IMAP APPEND */
      [client unselect];
      [client select: folderName];

      [(MAPIStoreMailFolder *) container synchroniseCache];
      changeKey = [properties objectForKey: MAPIPropertyKey (PR_CHANGE_KEY)];
      if (changeKey)
        {
          updatedMetadata = [(MAPIStoreMailFolder *) container updatePredecessorChangeListWith: changeKey
                                                                             forMessageWithKey: messageKey];
          if (!updatedMetadata)
            [self warnWithFormat: @"Predecessor change list not updated with client data"];
        }

      /* Update version property (PR_CHANGE_KEY indeed) as it is
         requested once it is saved */
      changeNumber = [(MAPIStoreMailFolder *) container changeNumberForMessageUID: newIdString];
      if (changeNumber)
        [properties setObject: [NSNumber numberWithUnsignedLongLong: [changeNumber unsignedLongLongValue] >> 16]
                       forKey: @"version"];
    }
}

@end
