/* MAPIStoreMailVolatileMessage.m - this file is part of SOGo * * Copyright (C) 2011-2012 Inverse inc * * Author: Wolfgang Sourdeau * * 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 #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #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 #include #include 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); } - (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 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) // bar, foo 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; } - (int) 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 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