Monotone-Parent: 3291dd3b0291e6b09f19d1879b5f7a56fd0f5045

Monotone-Revision: 529806b76290054de1e3af14d027b6f92e169941

Monotone-Author: wsourdeau@inverse.ca
Monotone-Date: 2007-10-30T19:46:48
Monotone-Branch: ca.inverse.sogo
maint-2.0.2
Wolfgang Sourdeau 2007-10-30 19:46:48 +00:00
parent cb09961275
commit 0986ba4ba1
6 changed files with 533 additions and 163 deletions

View File

@ -1,5 +1,26 @@
2007-10-30 Wolfgang Sourdeau <wsourdeau@inverse.ca>
* SoObjects/Mailer/NSString+Mail.m ([NSString -htmlToText]): new
method converting html content to plain text.
* SoObjects/Mailer/NSString+Mail.[hm]: new category module
enhancing NSString with utility methods pertaining to mail handling.
* SoObjects/Mailer/SOGoMailObject.m
([-shouldFetchPartOfType:_typesubtype:_subtype]): removed obsolete method.
([SOGoMailObject
-addRequiredKeysOfStructure:infopath:ptoArray:keysacceptedTypes:types]):
modified method to be always recursive and to take an array of the
accepted mime-types as parameter. The returned array now contains
the mime-type as well as the part keys.
* SoObjects/Mailer/SOGoMailObject+Draft.m ([SOGoMailObject
-contentForEditingOnParts:_prtskeys:_k]): removed obsolete method.
([SOGoMailObject -contentForEditing]): rewrote method to take into
account the first text/plain part or the first text/html part
converted to text/plain with our new -[NSString htmlToText]
category method.
* UI/MailerUI/UIxMailActions.m ([-replyToAllAction]): invoke
"replyToAll:" with YES as parameter instead of NO.

View File

@ -32,7 +32,8 @@ Mailer_OBJC_FILES += \
\
SOGoMailForward.m \
\
NSData+Mail.m
NSData+Mail.m \
NSString+Mail.m
Mailer_RESOURCE_FILES += \
Version \

View File

@ -0,0 +1,34 @@
/* NSString+Mail.h - this file is part of SOGo
*
* Copyright (C) 2007 Inverse groupe conseil
*
* 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 2, 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.
*/
#ifndef NSSTRING_MAIL_H
#define NSSTRING_MAIL_H
#import <Foundation/NSString.h>
@interface NSString (SOGoExtension)
- (NSString *) htmlToText;
@end
#endif /* NSSTRING_MAIL_H */

View File

@ -0,0 +1,351 @@
/* NSString+Mail.m - this file is part of SOGo
*
* Copyright (C) 2007 Inverse groupe conseil
*
* 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 2, 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.
*/
#import <Foundation/NSArray.h>
#import <Foundation/NSDictionary.h>
#import <Foundation/NSObject.h>
#import <SaxObjC/SaxAttributes.h>
#import <SaxObjC/SaxContentHandler.h>
#import <SaxObjC/SaxLexicalHandler.h>
#import <SaxObjC/SaxXMLReader.h>
#import <SaxObjC/SaxXMLReaderFactory.h>
#import <NGExtensions/NSString+misc.h>
#import <NGExtensions/NSObject+Logs.h>
#import "NSString+Mail.h"
#if 1
#define showWhoWeAre() \
[self logWithFormat: @"invoked '%@'", NSStringFromSelector (_cmd)]
#else
#define showWhoWeAre() {}
#endif
@interface _SOGoHTMLToTextContentHandler : NSObject <SaxContentHandler, SaxLexicalHandler>
{
NSArray *ignoreContentTags;
NSArray *specialTreatmentTags;
BOOL ignoreContent;
BOOL orderedList;
BOOL unorderedList;
unsigned int listCount;
NSMutableString *result;
}
+ (id) htmlToTextContentHandler;
- (NSString *) result;
@end
@implementation _SOGoHTMLToTextContentHandler
+ (id) htmlToTextContentHandler
{
static id htmlToTextContentHandler;
if (!htmlToTextContentHandler)
htmlToTextContentHandler = [self new];
return htmlToTextContentHandler;
}
- (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];
[ignoreContentTags retain];
[specialTreatmentTags retain];
ignoreContent = NO;
result = nil;
orderedList = NO;
unorderedList = NO;
listCount = 0;
}
return self;
}
- (void) dealloc
{
[ignoreContentTags release];
[specialTreatmentTags release];
[result release];
[super dealloc];
}
- (NSString *) result
{
NSString *newResult;
newResult = [NSString stringWithString: result];
[result release];
result = nil;
return newResult;
}
/* SaxContentHandler */
- (void) startDocument
{
showWhoWeAre();
[result release];
result = [NSMutableString new];
}
- (void) endDocument
{
showWhoWeAre();
ignoreContent = NO;
}
- (void) startPrefixMapping: (NSString *) prefix
uri: (NSString *) uri
{
showWhoWeAre();
}
- (void) endPrefixMapping: (NSString *) prefix
{
showWhoWeAre();
}
- (void) _startSpecialTreatment: (NSString *) tagName
{
if ([tagName isEqualToString: @"br"]
|| [tagName isEqualToString: @"p"])
[result appendString: @"\n"];
else if ([tagName isEqualToString: @"hr"])
[result appendString: @"______________________________________________________________________________\n"];
else if ([tagName isEqualToString: @"ul"])
{
[result appendString: @"\n"];
unorderedList = YES;
}
else if ([tagName isEqualToString: @"ol"])
{
[result appendString: @"\n"];
orderedList = YES;
listCount = 0;
}
else if ([tagName isEqualToString: @"li"])
{
if (orderedList)
{
listCount++;
[result appendFormat: @" %d. ", listCount];
}
else
[result appendString: @" * "];
}
else if ([tagName isEqualToString: @"dd"])
[result appendString: @" "];
}
- (void) _endSpecialTreatment: (NSString *) tagName
{
if ([tagName isEqualToString: @"ul"])
{
[result appendString: @"\n"];
unorderedList = NO;
}
else if ([tagName isEqualToString: @"ol"])
{
[result appendString: @"\n"];
orderedList = NO;
}
else if ([tagName isEqualToString: @"dt"])
{
[result appendString: @":\n"];
}
else if ([tagName isEqualToString: @"li"]
|| [tagName isEqualToString: @"dd"])
[result appendString: @"\n"];
}
- (void) startElement: (NSString *) element
namespace: (NSString *) namespace
rawName: (NSString *) rawName
attributes: (id <SaxAttributes>) attributes
{
NSString *tagName;
showWhoWeAre();
if (!ignoreContent)
{
tagName = [rawName lowercaseString];
if ([ignoreContentTags containsObject: tagName])
ignoreContent = YES;
else if ([specialTreatmentTags containsObject: tagName])
[self _startSpecialTreatment: tagName];
}
}
- (void) endElement: (NSString *) element
namespace: (NSString *) namespace
rawName: (NSString *) rawName
{
NSString *tagName;
showWhoWeAre();
if (ignoreContent)
{
tagName = [rawName lowercaseString];
if ([ignoreContentTags containsObject: tagName])
ignoreContent = NO;
else if ([specialTreatmentTags containsObject: tagName])
[self _endSpecialTreatment: tagName];
}
}
- (void) characters: (unichar *) characters
length: (int) length
{
if (!ignoreContent)
[result appendString: [NSString stringWithCharacters: characters
length: length]];
}
- (void) ignorableWhitespace: (unichar *) whitespaces
length: (int) length
{
showWhoWeAre();
}
- (void) processingInstruction: (NSString *) pi
data: (NSString *) data
{
showWhoWeAre();
}
- (void) setDocumentLocator: (id <NSObject, SaxLocator>) locator
{
showWhoWeAre();
}
- (void) skippedEntity: (NSString *) entity
{
showWhoWeAre();
}
/* SaxLexicalHandler */
- (void) comment: (unichar *) chars
length: (int) len
{
showWhoWeAre();
}
- (void) startDTD: (NSString *) name
publicId: (NSString *) pub
systemId: (NSString *) sys
{
showWhoWeAre();
}
- (void) endDTD
{
showWhoWeAre();
}
- (void) startEntity: (NSString *) entity
{
showWhoWeAre();
}
- (void) endEntity: (NSString *) entity
{
showWhoWeAre();
}
- (void) startCDATA
{
showWhoWeAre();
}
- (void) endCDATA
{
showWhoWeAre();
}
@end
// @interface NSDictionary (SOGoDebug)
// - (void) dump;
// @end
// @implementation NSDictionary (SOGoDebug)
// - (void) dump
// {
// NSEnumerator *keys;
// NSString *key;
// NSMutableString *dump;
// dump = [NSMutableString new];
// [dump appendFormat: @"\nNSDictionary dump (%@):\n", self];
// keys = [[self allKeys] objectEnumerator];
// key = [keys nextObject];
// while (key)
// {
// [dump appendFormat: @"%@: %@\n", key, [self objectForKey: key]];
// key = [keys nextObject];
// }
// [dump appendFormat: @"--- end ---\n"];
// NSLog (dump);
// [dump release];
// }
// @end
@implementation NSString (SOGoExtension)
- (NSString *) htmlToText
{
id <NSObject, SaxXMLReader> parser;
_SOGoHTMLToTextContentHandler *handler;
parser = [[SaxXMLReaderFactory standardXMLReaderFactory]
createXMLReaderForMimeType: @"text/html"];
handler = [_SOGoHTMLToTextContentHandler htmlToTextContentHandler];
[parser setContentHandler: handler];
[parser parseFromSource: self];
return [handler result];
}
@end

View File

@ -27,8 +27,10 @@
#import <NGExtensions/NSString+misc.h>
#import <NGExtensions/NSObject+Logs.h>
#import <SoObjects/SOGo/NSArray+Utilities.h>
#import <SoObjects/SOGo/SOGoUser.h>
#import "NSString+Mail.h"
#import "SOGoMailForward.h"
#import "SOGoMailObject+Draft.h"
@ -67,98 +69,54 @@
return newSubject;
}
- (NSString *) contentForEditingOnParts: (NSDictionary *) _prts
keys: (NSArray *) _k
- (NSString *) _contentForEditingFromKeys: (NSArray *) keys
{
static NSString *textPartSeparator = @"\n---\n";
NSMutableString *ms;
unsigned int count, max;
NSString *k, *v;
ms = [NSMutableString stringWithCapacity: 16000];
max = [_k count];
for (count = 0; count < max; count++)
{
k = [_k objectAtIndex: count];
// TODO: this is DUP code to SOGoMailObject
if ([k isEqualToString: @"body[text]"])
k = @"";
else if ([k hasPrefix: @"body["]) {
k = [k substringFromIndex: 5];
if ([k length] > 0)
k = [k substringToIndex: ([k length] - 1)];
}
v = [_prts objectForKey: k];
if ([v isKindOfClass: [NSString class]]
&& [v length] > 0)
{
if (count > 0)
[ms appendString: textPartSeparator];
[ms appendString: v];
}
else
[self logWithFormat:@"Note: cannot show part %@", k];
}
return ms;
}
#warning this method should be fixed to return the first available text/plain \
part, and otherwise the first text/html part converted to text
- (NSString *) contentForEditing
{
NSArray *keys;
NSArray *types;
NSDictionary *parts;
NSMutableArray *topLevelKeys = nil;
unsigned int count, max;
NSRange r;
NSString *contentForEditing;
NSString *rawPart, *content;
int index;
BOOL htmlContent;
// SOGoMailObject *co;
// co = self;
// keys = [co plainTextContentFetchKeys];
// infos = [co fetchCoreInfos];
// partInfos = [infos objectForKey: keys];
// NSLog (@"infos: '%@'", infos);
keys = [self plainTextContentFetchKeys];
max = [keys count];
if (max > 0)
if ([keys count])
{
if (max > 1)
types = [keys objectsForKey: @"mimeType"];
index = [types indexOfObject: @"text/plain"];
if (index == NSNotFound)
{
/* filter keys, only include top-level, or if none, the first */
for (count = 0; count < max; count++)
{
r = [[keys objectAtIndex: count] rangeOfString: @"."];
if (!r.length)
{
if (!topLevelKeys)
topLevelKeys = [NSMutableArray arrayWithCapacity: 4];
[topLevelKeys addObject: [keys objectAtIndex: count]];
}
}
if ([topLevelKeys count] > 0)
/* use top-level keys if we have some */
keys = topLevelKeys;
else
/* just take the first part */
keys = [NSArray arrayWithObject: [keys objectAtIndex: 0]];
index = [types indexOfObject: @"text/html"];
htmlContent = YES;
}
if (index == NSNotFound)
content = @"";
else
{
parts = [self fetchPlainTextStrings: keys];
rawPart = [[parts allValues] objectAtIndex: 0];
if (htmlContent)
content = [rawPart htmlToText];
else
content = rawPart;
}
parts = [self fetchPlainTextStrings: keys];
contentForEditing = [self contentForEditingOnParts: parts
keys: keys];
}
else
contentForEditing = nil;
content = @"";
return contentForEditing;
return content;
}
- (NSString *) contentForEditing
{
NSMutableArray *keys;
NSArray *acceptedTypes;
acceptedTypes
= [NSArray arrayWithObjects: @"text/plain", @"text/html", nil];
keys = [NSMutableArray new];
[self addRequiredKeysOfStructure: [self bodyStructure]
path: @"" toArray: keys acceptedTypes: acceptedTypes];
return [self _contentForEditingFromKeys: keys];
}
- (NSString *) contentForReply

View File

@ -40,6 +40,7 @@
#import <NGImap4/NGImap4EnvelopeAddress.h>
#import <NGMail/NGMimeMessageParser.h>
#import <SoObjects/SOGo/NSArray+Utilities.h>
#import <SoObjects/SOGo/SOGoPermissions.h>
#import <SoObjects/SOGo/SOGoUser.h>
@ -181,10 +182,13 @@ static BOOL debugSoParts = NO;
return ma;
}
- (NSArray *)toOneRelationshipKeys {
- (NSArray *) toOneRelationshipKeys
{
return [self relationshipKeysWithParts:NO];
}
- (NSArray *)toManyRelationshipKeys {
- (NSArray *) toManyRelationshipKeys
{
return [self relationshipKeysWithParts:YES];
}
@ -468,29 +472,29 @@ static BOOL debugSoParts = NO;
/* bulk fetching of plain/text content */
- (BOOL) shouldFetchPartOfType: (NSString *) _type
subtype: (NSString *) _subtype
{
/*
This method decides which parts are 'prefetched' for display. Those are
usually text parts (the set is currently hardcoded in this method ...).
*/
_type = [_type lowercaseString];
_subtype = [_subtype lowercaseString];
// - (BOOL) shouldFetchPartOfType: (NSString *) _type
// subtype: (NSString *) _subtype
// {
// /*
// This method decides which parts are 'prefetched' for display. Those are
// usually text parts (the set is currently hardcoded in this method ...).
// */
// _type = [_type lowercaseString];
// _subtype = [_subtype lowercaseString];
return (([_type isEqualToString: @"text"]
&& ([_subtype isEqualToString: @"plain"]
|| [_subtype isEqualToString: @"html"]
|| [_subtype isEqualToString: @"calendar"]))
|| ([_type isEqualToString: @"application"]
&& ([_subtype isEqualToString: @"pgp-signature"]
|| [_subtype hasPrefix: @"x-vnd.kolab."])));
}
// return (([_type isEqualToString: @"text"]
// && ([_subtype isEqualToString: @"plain"]
// || [_subtype isEqualToString: @"html"]
// || [_subtype isEqualToString: @"calendar"]))
// || ([_type isEqualToString: @"application"]
// && ([_subtype isEqualToString: @"pgp-signature"]
// || [_subtype hasPrefix: @"x-vnd.kolab."])));
// }
- (void) addRequiredKeysOfStructure: (id) _info
path: (NSString *) _p
toArray: (NSMutableArray *) _keys
recurse: (BOOL) _recurse
- (void) addRequiredKeysOfStructure: (NSDictionary *) info
path: (NSString *) p
toArray: (NSMutableArray *) keys
acceptedTypes: (NSArray *) types
{
/*
This is used to collect the set of IMAP4 fetch-keys required to fetch
@ -501,19 +505,19 @@ static BOOL debugSoParts = NO;
*/
NSArray *parts;
unsigned i, count;
BOOL fetchPart;
NSString *k;
id body;
NSString *sp;
NSString *sp, *mimeType;
id childInfo;
/* Note: if the part itself doesn't qualify, we still check subparts */
fetchPart = [self shouldFetchPartOfType: [_info valueForKey: @"type"]
subtype: [_info valueForKey: @"subtype"]];
if (fetchPart)
mimeType = [[NSString stringWithFormat: @"%@/%@",
[info valueForKey: @"type"],
[info valueForKey: @"subtype"]]
lowercaseString];
if ([types containsObject: mimeType])
{
if ([_p length] > 0)
k = [NSString stringWithFormat: @"body[%@]", _p];
if ([p length] > 0)
k = [NSString stringWithFormat: @"body[%@]", p];
else
{
/*
@ -523,40 +527,37 @@ static BOOL debugSoParts = NO;
*/
k = @"body[text]";
}
[_keys addObject: k];
[keys addObject: [NSDictionary dictionaryWithObjectsAndKeys: k, @"key",
mimeType, @"mimeType", nil]];
}
if (_recurse)
parts = [info objectForKey: @"parts"];
count = [parts count];
for (i = 0; i < count; i++)
{
/* recurse */
parts = [(NSDictionary *)_info objectForKey: @"parts"];
count = [parts count];
for (i = 0; i < count; i++)
{
sp = (([_p length] > 0)
? [_p stringByAppendingFormat: @".%d", i + 1]
: [NSString stringWithFormat: @"%d", i + 1]);
childInfo = [parts objectAtIndex: i];
[self addRequiredKeysOfStructure: childInfo
path: sp toArray: _keys
recurse: YES];
}
sp = (([p length] > 0)
? [p stringByAppendingFormat: @".%d", i + 1]
: [NSString stringWithFormat: @"%d", i + 1]);
/* check body */
body = [(NSDictionary *)_info objectForKey: @"body"];
if (body)
{
sp = [[body valueForKey: @"type"] lowercaseString];
if ([sp isEqualToString: @"multipart"])
sp = _p;
else
sp = [_p length] > 0 ? [_p stringByAppendingString: @".1"] : @"1";
[self addRequiredKeysOfStructure: body
path: sp toArray: _keys
recurse: YES];
}
childInfo = [parts objectAtIndex: i];
[self addRequiredKeysOfStructure: childInfo
path: sp toArray: keys
acceptedTypes: types];
}
/* check body */
body = [info objectForKey: @"body"];
if (body)
{
sp = [[body valueForKey: @"type"] lowercaseString];
if ([sp isEqualToString: @"multipart"])
sp = p;
else
sp = [p length] > 0 ? [p stringByAppendingString: @".1"] : @"1";
[self addRequiredKeysOfStructure: body
path: sp toArray: keys
acceptedTypes: types];
}
}
@ -567,10 +568,13 @@ static BOOL debugSoParts = NO;
keys which are marked by the -shouldFetchPartOfType:subtype: method.
*/
NSMutableArray *ma;
ma = [NSMutableArray arrayWithCapacity:4];
NSArray *types;
types = [NSArray arrayWithObjects: @"text/plain", @"text/html",
@"text/calendar", @"application/pgp-signature", nil];
ma = [NSMutableArray arrayWithCapacity: 4];
[self addRequiredKeysOfStructure: [self bodyStructure]
path: @"" toArray: ma recurse: YES];
path: @"" toArray: ma acceptedTypes: types];
return ma;
}
@ -584,7 +588,7 @@ static BOOL debugSoParts = NO;
[self debugWithFormat: @"fetch keys: %@", _fetchKeys];
result = [self fetchParts:_fetchKeys];
result = [self fetchParts: [_fetchKeys objectsForKey: @"key"]];
result = [result valueForKey: @"RawResponse"]; // hackish
// Note: -valueForKey: doesn't work!
@ -596,7 +600,7 @@ static BOOL debugSoParts = NO;
NSString *key;
NSData *data;
key = [_fetchKeys objectAtIndex:i];
key = [[_fetchKeys objectAtIndex:i] objectForKey: @"key"];
data = [(NSDictionary *)[(NSDictionary *)result objectForKey:key]
objectForKey: @"data"];
@ -622,7 +626,7 @@ static BOOL debugSoParts = NO;
- (NSDictionary *) fetchPlainTextParts
{
return [self fetchPlainTextParts:[self plainTextContentFetchKeys]];
return [self fetchPlainTextParts: [self plainTextContentFetchKeys]];
}
/* convert parts to strings */
@ -656,19 +660,20 @@ static BOOL debugSoParts = NO;
- (NSDictionary *) stringifyTextParts: (NSDictionary *) _datas
{
NSMutableDictionary *md;
NSDictionary *info;
NSEnumerator *keys;
NSString *key;
md = [NSMutableDictionary dictionaryWithCapacity:4];
NSString *key, *s;
md = [NSMutableDictionary dictionaryWithCapacity:4];
keys = [_datas keyEnumerator];
while ((key = [keys nextObject]) != nil) {
NSDictionary *info;
NSString *s;
info = [self lookupInfoForBodyPart:key];
if ((s = [self stringForData:[_datas objectForKey:key] partInfo:info]))
[md setObject:s forKey:key];
}
while ((key = [keys nextObject]))
{
info = [self lookupInfoForBodyPart: key];
s = [self stringForData: [_datas objectForKey:key] partInfo: info];
if (s)
[md setObject: s forKey: key];
}
return md;
}