<img src=data...> conversion to file attachments + CIDs.

pull/17/merge
Ludovic Marcotte 2013-11-20 08:56:29 -05:00
parent 6587f4f193
commit e4aedbac08
7 changed files with 336 additions and 57 deletions

13
NEWS
View File

@ -1,3 +1,16 @@
2.1.2 (2013-11-XX)
------------------
New features
-
Enhancements
- we now automatically convert <img src=data...> into file attachments
using CIDs. This prevents Outlook issues.
Bug fixes
-
2.1.1 (2013-11-19)
------------------

View File

@ -21,12 +21,16 @@
#ifndef NSSTRING_MAIL_H
#define NSSTRING_MAIL_H
#import <Foundation/NSArray.h>
#import <Foundation/NSString.h>
@interface NSString (SOGoExtension)
- (NSString *) htmlToText;
- (NSString *) htmlByExtractingImages: (NSMutableArray *) theImages;
- (NSString *) stringByConvertingCRLNToHTML;
- (int) indexOf: (unichar) _c
fromIndex: (int) start;
- (int) indexOf: (unichar) _c;
- (NSString *) decodedHeader;

View File

@ -22,19 +22,25 @@
#import <Foundation/NSDictionary.h>
#import <Foundation/NSObject.h>
#import <Foundation/NSException.h>
#import <Foundation/NSValue.h>
#import <SaxObjC/SaxAttributes.h>
#import <SaxObjC/SaxContentHandler.h>
#import <SaxObjC/SaxLexicalHandler.h>
#import <SaxObjC/SaxXMLReader.h>
#import <SaxObjC/SaxXMLReaderFactory.h>
#import <NGExtensions/NGHashMap.h>
#import <NGExtensions/NGQuotedPrintableCoding.h>
#import <NGExtensions/NSString+misc.h>
#import <NGExtensions/NSObject+Logs.h>
#import <NGMime/NGMimeBodyPart.h>
#import <NGMime/NGMimeFileData.h>
#include <libxml/encoding.h>
#import "NSString+Mail.h"
#import "NSData+Mail.h"
#import "../SOGo/SOGoObject.h"
#if 0
#define showWhoWeAre() \
@ -43,11 +49,15 @@
#define showWhoWeAre() {}
#endif
@interface _SOGoHTMLToTextContentHandler : NSObject <SaxContentHandler, SaxLexicalHandler>
#define paddingBuffer 8192
@interface _SOGoHTMLContentHandler : NSObject <SaxContentHandler, SaxLexicalHandler>
{
NSMutableArray *images;
NSArray *ignoreContentTags;
NSArray *specialTreatmentTags;
BOOL ignoreContent;
BOOL orderedList;
BOOL unorderedList;
@ -57,32 +67,26 @@
}
+ (id) htmlToTextContentHandler;
+ (id) sanitizerContentHandler;
- (NSString *) result;
- (void) setIgnoreContentTags: (NSArray *) theTags;
- (void) setSpecialTreatmentTags: (NSArray *) theTags;
- (void) setImages: (NSMutableArray *) theImages;
@end
@implementation _SOGoHTMLToTextContentHandler
+ (id) htmlToTextContentHandler
{
static id htmlToTextContentHandler;
if (!htmlToTextContentHandler)
htmlToTextContentHandler = [self new];
return htmlToTextContentHandler;
}
@implementation _SOGoHTMLContentHandler
- (id) init
{
if ((self = [super init]))
{
ignoreContentTags = [NSArray arrayWithObjects: @"head", @"script",
@"style", nil];
specialTreatmentTags = [NSArray arrayWithObjects: @"body", @"p", @"ul",
@"li", @"table", @"tr", @"td", @"th",
@"br", @"hr", @"dt", @"dd", nil];
images = nil;
ignoreContentTags = nil;
specialTreatmentTags = nil;
[ignoreContentTags retain];
[specialTreatmentTags retain];
@ -97,6 +101,32 @@
return self;
}
+ (id) htmlToTextContentHandler
{
static id htmlToTextContentHandler;
if (!htmlToTextContentHandler)
htmlToTextContentHandler = [self new];
[htmlToTextContentHandler setIgnoreContentTags: [NSArray arrayWithObjects: @"head", @"script",
@"style", nil]];
[htmlToTextContentHandler setSpecialTreatmentTags: [NSArray arrayWithObjects: @"body", @"p", @"ul",
@"li", @"table", @"tr", @"td", @"th",
@"br", @"hr", @"dt", @"dd", nil]];
return htmlToTextContentHandler;
}
+ (id) sanitizerContentHandler
{
static id sanitizerContentHandler;
if (!sanitizerContentHandler)
sanitizerContentHandler = [self new];
return sanitizerContentHandler;
}
- (xmlCharEncoding) contentEncoding
{
return XML_CHAR_ENCODING_UTF8;
@ -121,6 +151,25 @@
return newResult;
}
- (void) setIgnoreContentTags: (NSArray *) theTags
{
ASSIGN(ignoreContentTags, theTags);
}
- (void) setSpecialTreatmentTags: (NSArray *) theTags
{
ASSIGN(specialTreatmentTags, theTags);
}
//
// We MUST NOT retain the array here
//
- (void) setImages: (NSMutableArray *) theImages
{
images = theImages;
}
/* SaxContentHandler */
- (void) startDocument
{
@ -210,14 +259,89 @@
showWhoWeAre();
if (!ignoreContent)
tagName = [rawName lowercaseString];
if (!ignoreContent && ignoreContentTags && specialTreatmentTags)
{
tagName = [rawName lowercaseString];
if ([ignoreContentTags containsObject: tagName])
ignoreContent = YES;
else if ([specialTreatmentTags containsObject: tagName])
[self _startSpecialTreatment: tagName];
}
else if ([tagName isEqualToString: @"img"])
{
NSString *value;
value = [attributes valueForRawName: @"src"];
//
// Check for Data URI Scheme
//
// data:[<MIME-type>][;charset=<encoding>][;base64],<data>
//
if ([value length] > 5 && [[value substringToIndex: 5] caseInsensitiveCompare: @"data:"] == NSOrderedSame)
{
NSString *uniqueId, *mimeType, *encoding, *charset;
NGMimeBodyPart *bodyPart;
NGMutableHashMap *map;
NSData *data;
id body;
int i, j, k;
i = [value indexOf: ';'];
j = [value indexOf: ';' fromIndex: i+1];
k = [value indexOf: ','];
// We try to get the MIME type
mimeType = nil;
if (i > 5 && i < k)
{
mimeType = [value substringWithRange: NSMakeRange(5, i-5)];
}
else
i = 5;
// We might get a stupid value. We discard anything that doesn't have a / in it
if ([mimeType indexOf: '/'] < 0)
mimeType = @"image/jpeg";
// We check and skip the charset
if (j > i)
charset = [value substringWithRange: NSMakeRange(i+1, j-i-1)];
else
j = i;
// We check the encoding and we completely ignore it
encoding = [value substringWithRange: NSMakeRange(j+1, k-j-1)];
if (![encoding length])
encoding = @"base64";
data = [[value substringFromIndex: k+1] dataUsingEncoding: NSASCIIStringEncoding];
uniqueId = [SOGoObject globallyUniqueObjectId];
map = [[[NGMutableHashMap alloc] initWithCapacity:5] autorelease];
[map setObject: encoding forKey: @"content-transfer-encoding"];
[map setObject:[NSNumber numberWithInt:[data length]] forKey: @"content-length"];
[map setObject: [NSString stringWithFormat: @"inline; filename=\"%@\"", uniqueId] forKey: @"content-disposition"];
[map setObject: [NSString stringWithFormat: @"%@; name=\"%@\"", mimeType, uniqueId] forKey: @"content-type"];
[map setObject: [NSString stringWithFormat: @"<%@>", uniqueId] forKey: @"content-id"];
body = [[NGMimeFileData alloc] initWithBytes: [data bytes] length: [data length]];
bodyPart = [[[NGMimeBodyPart alloc] initWithHeader:map] autorelease];
[bodyPart setBody: body];
[body release];
[images addObject: bodyPart];
[result appendFormat: @"<img src=\"cid:%@\" type=\"%@\">", uniqueId, mimeType];
}
}
}
- (void) endElement: (NSString *) element
@ -228,7 +352,7 @@
showWhoWeAre();
if (ignoreContent)
if (ignoreContent && ignoreContentTags && specialTreatmentTags)
{
tagName = [rawName lowercaseString];
if ([ignoreContentTags containsObject: tagName])
@ -345,13 +469,13 @@
- (NSString *) htmlToText
{
_SOGoHTMLToTextContentHandler *handler;
_SOGoHTMLContentHandler *handler;
id <NSObject, SaxXMLReader> parser;
NSData *d;
parser = [[SaxXMLReaderFactory standardXMLReaderFactory]
createXMLReaderForMimeType: @"text/html"];
handler = [_SOGoHTMLToTextContentHandler htmlToTextContentHandler];
handler = [_SOGoHTMLContentHandler htmlToTextContentHandler];
[parser setContentHandler: handler];
d = [self dataUsingEncoding: NSUTF8StringEncoding];
@ -360,7 +484,24 @@
return [handler result];
}
#define paddingBuffer 8192
- (NSString *) htmlByExtractingImages: (NSMutableArray *) theImages
{
_SOGoHTMLContentHandler *handler;
id <NSObject, SaxXMLReader> parser;
NSData *d;
parser = [[SaxXMLReaderFactory standardXMLReaderFactory]
createXMLReaderForMimeType: @"text/html"];
handler = [_SOGoHTMLContentHandler sanitizerContentHandler];
[handler setImages: theImages];
[parser setContentHandler: handler];
d = [self dataUsingEncoding: NSUTF8StringEncoding];
[parser parseFromSource: d];
return [handler result];
}
static inline char *
convertChars (const char *oldString, unsigned int oldLength,
@ -434,18 +575,29 @@ convertChars (const char *oldString, unsigned int oldLength,
return convertedString;
}
- (int) indexOf: (unichar) _c
fromIndex: (int) start
{
int i, len;
len = [self length];
for (i = 0; i < len; i++)
if (start < 0 || start >= len)
start = 0;
for (i = start; i < len; i++)
{
if ([self characterAtIndex: i] == _c) return i;
}
return -1;
}
- (int) indexOf: (unichar) _c
{
return [self indexOf: _c fromIndex: 0];
}
- (NSString *) decodedHeader

View File

@ -1,5 +1,5 @@
/*
Copyright (C) 2007-2012 Inverse inc.
Copyright (C) 2007-2013 Inverse inc.
Copyright (C) 2004-2005 SKYRIX Software AG
This file is part of SOGo.
@ -395,11 +395,17 @@ static NSString *userAgent = nil;
[self setSourceFolder: [paths componentsJoinedByString: @"/"]];
}
//
//
//
- (NSString *) sourceFolder
{
return sourceFolder;
}
//
//
//
- (NSException *) storeInfo
{
NSMutableDictionary *infos;
@ -446,6 +452,9 @@ static NSString *userAgent = nil;
return error;
}
//
//
//
- (void) _loadInfosFromDictionary: (NSDictionary *) infoDict
{
id value;
@ -478,11 +487,17 @@ static NSString *userAgent = nil;
[self setInReplyTo: value];
}
//
//
//
- (NSString *) relativeImap4Name
{
return [NSString stringWithFormat: @"%d", IMAP4ID];
}
//
//
//
- (void) fetchInfo
{
NSString *p;
@ -504,16 +519,25 @@ static NSString *userAgent = nil;
[self debugWithFormat: @"Note: info object does not yet exist: %@", p];
}
//
//
//
- (void) setIMAP4ID: (int) newIMAP4ID
{
IMAP4ID = newIMAP4ID;
}
//
//
//
- (int) IMAP4ID
{
return IMAP4ID;
}
//
//
//
- (NSException *) save
{
NGImap4Client *client;
@ -552,6 +576,9 @@ static NSString *userAgent = nil;
return error;
}
//
//
//
- (void) _addEMailsOfAddresses: (NSArray *) _addrs
toArray: (NSMutableArray *) _ma
{
@ -564,6 +591,9 @@ static NSString *userAgent = nil;
[_ma addObject: [currentAddress email]];
}
//
//
//
- (void) _addRecipients: (NSArray *) recipients
toArray: (NSMutableArray *) array
{
@ -576,6 +606,9 @@ static NSString *userAgent = nil;
[array addObject: [currentAddress baseEMail]];
}
//
//
//
- (void) _purgeRecipients: (NSArray *) recipients
fromAddresses: (NSMutableArray *) addresses
{
@ -602,6 +635,9 @@ static NSString *userAgent = nil;
}
}
//
//
//
- (void) _fillInReplyAddresses: (NSMutableDictionary *) _info
replyToAll: (BOOL) _replyToAll
envelope: (NGImap4Envelope *) _envelope
@ -711,6 +747,9 @@ static NSString *userAgent = nil;
}
}
//
//
//
- (NSArray *) _attachmentBodiesFromPaths: (NSArray *) paths
fromResponseFetch: (NSDictionary *) fetch;
{
@ -731,6 +770,9 @@ static NSString *userAgent = nil;
return bodies;
}
//
//
//
- (void) _fetchAttachments: (NSArray *) parts
fromMail: (SOGoMailObject *) sourceMail
{
@ -758,6 +800,9 @@ static NSString *userAgent = nil;
}
}
//
//
//
- (void) fetchMailForEditing: (SOGoMailObject *) sourceMail
{
NSString *subject, *msgid;
@ -804,6 +849,9 @@ static NSString *userAgent = nil;
[self storeInfo];
}
//
//
//
- (void) fetchMailForReplying: (SOGoMailObject *) sourceMail
toAll: (BOOL) toAll
{
@ -1255,6 +1303,9 @@ static NSString *userAgent = nil;
return bodyPart;
}
//
//
//
- (NSArray *) bodyPartsForAllAttachments
{
/* returns nil on error */
@ -1276,6 +1327,9 @@ static NSString *userAgent = nil;
return bodyParts;
}
//
//
//
- (NGMimeBodyPart *) mimeMultipartAlternative
{
NGMimeMultipartBody *textParts;
@ -1301,6 +1355,9 @@ static NSString *userAgent = nil;
return part;
}
//
//
//
- (NGMimeMessage *) mimeMultiPartMessageWithHeaderMap: (NGMutableHashMap *) map
andBodyParts: (NSArray *) _bodyParts
{
@ -1340,6 +1397,9 @@ static NSString *userAgent = nil;
return message;
}
//
//
//
- (void) _addHeaders: (NSDictionary *) _h
toHeaderMap: (NGMutableHashMap *) _map
{
@ -1511,48 +1571,59 @@ static NSString *userAgent = nil;
return map;
}
//
//
//
- (NGMimeMessage *) mimeMessageWithHeaders: (NSDictionary *) _headers
excluding: (NSArray *) _exclude
{
NGMutableHashMap *map;
NSArray *bodyParts;
NGMimeMessage *message;
NSMutableArray *bodyParts;
NGMimeMessage *message;
NGMutableHashMap *map;
NSString *newText;
message = nil;
bodyParts = [NSMutableArray array];
newText = [text htmlByExtractingImages: bodyParts];
if ([bodyParts count])
[self setText: newText];
map = [self mimeHeaderMapWithHeaders: _headers
excluding: _exclude];
if (map)
{
//[self debugWithFormat: @"MIME Envelope: %@", map];
bodyParts = [self bodyPartsForAllAttachments];
if (bodyParts)
{
//[self debugWithFormat: @"attachments: %@", bodyParts];
if ([bodyParts count] == 0)
/* no attachments */
message = [self mimeMessageForContentWithHeaderMap: map];
else
/* attachments, create multipart/mixed */
message = [self mimeMultiPartMessageWithHeaderMap: map
andBodyParts: bodyParts];
//[self debugWithFormat: @"message: %@", message];
}
[bodyParts addObjectsFromArray: [self bodyPartsForAllAttachments]];
//[self debugWithFormat: @"attachments: %@", bodyParts];
if ([bodyParts count] == 0)
/* no attachments */
message = [self mimeMessageForContentWithHeaderMap: map];
else
[self errorWithFormat:
@"could not create body parts for attachments!"];
/* attachments, create multipart/mixed */
message = [self mimeMultiPartMessageWithHeaderMap: map
andBodyParts: bodyParts];
//[self debugWithFormat: @"message: %@", message];
}
return message;
}
//
//
//
- (NGMimeMessage *) mimeMessage
{
return [self mimeMessageWithHeaders: nil excluding: nil];
}
//
//
//
- (NSData *) mimeMessageAsData
{
NGMimeMessageGenerator *generator;
@ -1565,6 +1636,9 @@ static NSString *userAgent = nil;
return message;
}
//
//
//
- (NSArray *) allRecipients
{
NSMutableArray *allRecipients;
@ -1584,6 +1658,9 @@ static NSString *userAgent = nil;
return allRecipients;
}
//
//
//
- (NSArray *) allBareRecipients
{
NSMutableArray *bareRecipients;
@ -1599,11 +1676,17 @@ static NSString *userAgent = nil;
return bareRecipients;
}
//
//
//
- (NSException *) sendMail
{
return [self sendMailAndCopyToSent: YES];
}
//
//
//
- (NSException *) sendMailAndCopyToSent: (BOOL) copyToSent
{
NSMutableData *cleaned_message;

View File

@ -42,6 +42,9 @@
#define maxFilenameLength 64
//
//
//
@implementation SOGoMailObject (SOGoDraftObjectExtensions)
- (NSString *) subjectForReply
@ -77,6 +80,9 @@
return newSubject;
}
//
//
//
- (NSString *) _convertRawContentForEditing: (NSString *) raw
rawHtml: (BOOL) html
{
@ -96,6 +102,9 @@
return rc;
}
//
//
//
- (NSString *) _contentForEditingFromKeys: (NSArray *) keys
{
NSArray *types;
@ -151,6 +160,9 @@
return content;
}
//
//
//
- (NSString *) contentForEditing
{
NSMutableArray *keys;
@ -166,6 +178,9 @@
return [self _contentForEditingFromKeys: keys];
}
//
//
//
- (NSString *) contentForReply
{
NSString *pageName;
@ -185,6 +200,9 @@
return [[page generateResponse] contentAsString];
}
//
//
//
- (NSString *) filenameForForward
{
NSString *subject;
@ -218,6 +236,9 @@
return newSubject;
}
//
//
//
- (NSString *) subjectForForward
{
NSString *subject, *newSubject;
@ -231,6 +252,9 @@
return newSubject;
}
//
//
//
- (NSString *) contentForInlineForward
{
SOGoUserDefaults *ud;
@ -247,6 +271,9 @@
return [[page generateResponse] contentAsString];
}
//
//
//
- (void) _fetchFileAttachmentKey: (NSDictionary *) part
intoArray: (NSMutableArray *) keys
withPath: (NSString *) path
@ -291,6 +318,9 @@
}
}
//
//
//
- (void) _fetchFileAttachmentKeysInPart: (NSDictionary *) part
intoArray: (NSMutableArray *) keys
withPath: (NSString *) path
@ -325,6 +355,9 @@
}
}
//
//
//
#warning we might need to handle parts with a "name" attribute
- (NSArray *) fetchFileAttachmentKeys
{

View File

@ -1,8 +1,6 @@
/* UIxMailPartHTMLViewer.h - this file is part of SOGo
*
* Copyright (C) 2007, 2008 Inverse inc.
*
* Author: Wolfgang Sourdeau <wsourdeau@inverse.ca>
* Copyright (C) 2007-2013 Inverse inc.
*
* 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

View File

@ -1,10 +1,6 @@
/* UIxMailPartHTMLViewer.m - this file is part of SOGo
*
* Copyright (C) 2007-2012 Inverse inc.
*
* Author: Wolfgang Sourdeau <wsourdeau@inverse.ca>
* Ludovic Marcotte <lmarcotte@inverse.ca>
* Francis Lachapelle <flachapelle@inverse.ca>
* Copyright (C) 2007-2013 Inverse inc.
*
* 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