sogo/OpenChange/MAPIStoreMailVolatileMessage.m

1182 lines
37 KiB
Objective-C

/* 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
{
return ([properties
objectForKey: MAPIPropertyKey (PR_ATTACH_CONTENT_ID_UNICODE)]
!= nil);
}
- (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)
[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);
}
- (int) getPidTagMessageClass: (void **) data inMemCtx: (TALLOC_CTX *) memCtx
{
*data = [@"IPM.Note" asUnicodeInMemCtx: memCtx];
return MAPISTORE_SUCCESS;
}
- (int) getPidTagChangeKey: (void **) data inMemCtx: (TALLOC_CTX *) memCtx
{
NSData *changeKey;
int 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 ldb_context *samCtx;
struct mapistore_message *msgData;
struct mapistore_message_recipient *recipient;
enum ulRecipClass type;
samCtx = [[self context] connectionInfo]->sam_ctx;
// [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 (samCtx, 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)
{
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 (connInfo->sam_ctx, &bin32);
if (addrBookEntryId && [[NSString stringWithGUID: &addrBookEntryId->ProviderUID]
hasSuffix: @"08002b2fe182"])
{
/* TODO: better way to distinguish local and other ones */
username = MAPIStoreSamDBUserAttribute (connInfo->sam_ctx, @"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 (connInfo->sam_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"];
}
talloc_free (oneOffEntryId);
}
talloc_free (addrBookEntryId);
if ([[fromRecipient allKeys] count] > 0)
{
list = MakeRecipientsList ([NSArray arrayWithObjects: fromRecipient, nil]);
if ([list count])
[headers setObjects: list forKey: @"from"];
}
}
}
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;
}
- (int) submitWithFlags: (enum SubmitFlags) flags
{
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 logWithFormat: @"an error occurred: '%@'", error];
// 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 MAPISTORE_SUCCESS;
}
- (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