From 0986ba4ba1d0f8f8120e1be537b9e5018254c965 Mon Sep 17 00:00:00 2001 From: Wolfgang Sourdeau Date: Tue, 30 Oct 2007 19:46:48 +0000 Subject: [PATCH] Monotone-Parent: 3291dd3b0291e6b09f19d1879b5f7a56fd0f5045 Monotone-Revision: 529806b76290054de1e3af14d027b6f92e169941 Monotone-Author: wsourdeau@inverse.ca Monotone-Date: 2007-10-30T19:46:48 Monotone-Branch: ca.inverse.sogo --- ChangeLog | 21 ++ SoObjects/Mailer/GNUmakefile | 3 +- SoObjects/Mailer/NSString+Mail.h | 34 +++ SoObjects/Mailer/NSString+Mail.m | 351 ++++++++++++++++++++++++ SoObjects/Mailer/SOGoMailObject+Draft.m | 122 +++----- SoObjects/Mailer/SOGoMailObject.m | 163 +++++------ 6 files changed, 532 insertions(+), 162 deletions(-) create mode 100644 SoObjects/Mailer/NSString+Mail.h create mode 100644 SoObjects/Mailer/NSString+Mail.m diff --git a/ChangeLog b/ChangeLog index 446f3fdc1..1c2dcc2e3 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,5 +1,26 @@ 2007-10-30 Wolfgang Sourdeau + * 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. diff --git a/SoObjects/Mailer/GNUmakefile b/SoObjects/Mailer/GNUmakefile index 381e3fbdd..bd94d2e0e 100644 --- a/SoObjects/Mailer/GNUmakefile +++ b/SoObjects/Mailer/GNUmakefile @@ -32,7 +32,8 @@ Mailer_OBJC_FILES += \ \ SOGoMailForward.m \ \ - NSData+Mail.m + NSData+Mail.m \ + NSString+Mail.m Mailer_RESOURCE_FILES += \ Version \ diff --git a/SoObjects/Mailer/NSString+Mail.h b/SoObjects/Mailer/NSString+Mail.h new file mode 100644 index 000000000..254711015 --- /dev/null +++ b/SoObjects/Mailer/NSString+Mail.h @@ -0,0 +1,34 @@ +/* NSString+Mail.h - this file is part of SOGo + * + * Copyright (C) 2007 Inverse groupe conseil + * + * 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 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 + +@interface NSString (SOGoExtension) + +- (NSString *) htmlToText; + +@end + +#endif /* NSSTRING_MAIL_H */ diff --git a/SoObjects/Mailer/NSString+Mail.m b/SoObjects/Mailer/NSString+Mail.m new file mode 100644 index 000000000..494becdc0 --- /dev/null +++ b/SoObjects/Mailer/NSString+Mail.m @@ -0,0 +1,351 @@ +/* NSString+Mail.m - this file is part of SOGo + * + * Copyright (C) 2007 Inverse groupe conseil + * + * 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 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 +#import +#import +#import +#import +#import +#import +#import +#import +#import + +#import "NSString+Mail.h" + +#if 1 +#define showWhoWeAre() \ + [self logWithFormat: @"invoked '%@'", NSStringFromSelector (_cmd)] +#else +#define showWhoWeAre() {} +#endif + +@interface _SOGoHTMLToTextContentHandler : NSObject +{ + 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 ) 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 ) 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 parser; + _SOGoHTMLToTextContentHandler *handler; + + parser = [[SaxXMLReaderFactory standardXMLReaderFactory] + createXMLReaderForMimeType: @"text/html"]; + handler = [_SOGoHTMLToTextContentHandler htmlToTextContentHandler]; + [parser setContentHandler: handler]; + [parser parseFromSource: self]; + + return [handler result]; +} + +@end diff --git a/SoObjects/Mailer/SOGoMailObject+Draft.m b/SoObjects/Mailer/SOGoMailObject+Draft.m index ebe7e04a7..66ffa4447 100644 --- a/SoObjects/Mailer/SOGoMailObject+Draft.m +++ b/SoObjects/Mailer/SOGoMailObject+Draft.m @@ -27,8 +27,10 @@ #import #import +#import #import +#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; + 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 - /* just take the first part */ - keys = [NSArray arrayWithObject: [keys objectAtIndex: 0]]; + 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 diff --git a/SoObjects/Mailer/SOGoMailObject.m b/SoObjects/Mailer/SOGoMailObject.m index c86781f59..15d531375 100644 --- a/SoObjects/Mailer/SOGoMailObject.m +++ b/SoObjects/Mailer/SOGoMailObject.m @@ -40,6 +40,7 @@ #import #import +#import #import #import @@ -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; }