]> err.no Git - scalable-opengroupware.org/blob - UI/MailPartViewers/UIxMailPartHTMLViewer.m
git-svn-id: http://svn.opengroupware.org/SOGo/inverse/trunk@1305 d1b88da0-ebda-0310...
[scalable-opengroupware.org] / UI / MailPartViewers / UIxMailPartHTMLViewer.m
1 /* UIxMailPartHTMLViewer.m - this file is part of SOGo
2  *
3  * Copyright (C) 2007 Inverse groupe conseil
4  *
5  * Author: Wolfgang Sourdeau <wsourdeau@inverse.ca>
6  *
7  * This file is free software; you can redistribute it and/or modify
8  * it under the terms of the GNU General Public License as published by
9  * the Free Software Foundation; either version 2, or (at your option)
10  * any later version.
11  *
12  * This file is distributed in the hope that it will be useful,
13  * but WITHOUT ANY WARRANTY; without even the implied warranty of
14  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15  * GNU General Public License for more details.
16  *
17  * You should have received a copy of the GNU General Public License
18  * along with this program; see the file COPYING.  If not, write to
19  * the Free Software Foundation, Inc., 59 Temple Place - Suite 330,
20  * Boston, MA 02111-1307, USA.
21  */
22
23 #import <Foundation/NSArray.h>
24 #import <Foundation/NSDictionary.h>
25 #import <Foundation/NSKeyValueCoding.h>
26 #import <Foundation/NSValue.h>
27
28 #import <SaxObjC/SaxAttributes.h>
29 #import <SaxObjC/SaxContentHandler.h>
30 #import <SaxObjC/SaxLexicalHandler.h>
31 #import <SaxObjC/SaxXMLReader.h>
32 #import <SaxObjC/SaxXMLReaderFactory.h>
33 #import <NGExtensions/NSString+misc.h>
34 #import <NGObjWeb/SoObjects.h>
35
36 #include <libxml/encoding.h>
37
38 #import <SoObjects/Mailer/SOGoMailObject.h>
39
40 #import "UIxMailPartHTMLViewer.h"
41
42 #if 0
43 #define showWhoWeAre() NSLog(@"invoked '%@'", NSStringFromSelector(_cmd))
44 #else
45 #define showWhoWeAre()
46 #endif
47
48 @interface _UIxHTMLMailContentHandler : NSObject <SaxContentHandler, SaxLexicalHandler>
49 {
50   NSMutableString *result;
51   NSMutableString *css;
52   NSDictionary *attachmentIds;
53   BOOL inBody;
54   BOOL inStyle;
55   BOOL inScript;
56   BOOL inCSSDeclaration;
57   BOOL hasEmbeddedCSS;
58   NSMutableArray *crumb;
59   xmlCharEncoding contentEncoding;
60 }
61
62 - (NSString *) result;
63
64 @end
65
66 @implementation _UIxHTMLMailContentHandler
67
68 - (id) init
69 {
70   if ((self = [super init]))
71     {
72       crumb = nil;
73       css = nil;
74       result = nil;
75       attachmentIds = nil;
76       contentEncoding = XML_CHAR_ENCODING_UTF8;
77     }
78
79   return self;
80 }
81
82 - (void) dealloc
83 {
84   [crumb release];
85   [result release];
86   [css release];
87   [super dealloc];
88 }
89
90 - (void) setContentEncoding: (xmlCharEncoding) newContentEncoding
91 {
92   contentEncoding = newContentEncoding;
93 }
94
95 - (xmlCharEncoding) contentEncoding
96 {
97   return contentEncoding;
98 }
99
100 - (void) setAttachmentIds: (NSDictionary *) newAttachmentIds
101 {
102   attachmentIds = newAttachmentIds;
103 }
104
105 - (NSString *) css
106 {
107   return css;
108 }
109
110 - (NSString *) result
111 {
112   return result;
113 }
114
115 /* SaxContentHandler */
116 - (void) startDocument
117 {
118   showWhoWeAre();
119
120   [crumb release];
121   [css release];
122   [result release];
123
124   result = [NSMutableString new];
125   css = [NSMutableString new];
126   crumb = [NSMutableArray new];
127
128   inBody = NO;
129   inStyle = NO;
130   inScript = NO;
131   inCSSDeclaration = NO;
132   hasEmbeddedCSS = NO;
133 }
134
135 - (void) endDocument
136 {
137   unsigned int count, max;
138
139
140   showWhoWeAre();
141   max = [crumb count];
142   if (max > 0)
143     for (count = max - 1; count > -1; count--)
144       {
145         [result appendFormat: @"</%@>", [crumb objectAtIndex: count]];
146         [crumb removeObjectAtIndex: count];
147       }
148 }
149
150 - (void) startPrefixMapping: (NSString *)_prefix
151                         uri: (NSString *)_uri
152 {
153   showWhoWeAre();
154 }
155
156 - (void) endPrefixMapping: (NSString *)_prefix
157 {
158   showWhoWeAre();
159 }
160
161 - (void) _appendStyle: (unichar *) _chars
162                length: (int) _len
163 {
164   unsigned int count;
165   unichar *start, *currentChar;
166
167   start = _chars;
168   currentChar = start;
169   for (count = 0; count < _len; count++)
170     {
171       currentChar = _chars + count;
172       if (inCSSDeclaration)
173         {
174           if (*(char *) currentChar == '}')
175             {
176               inCSSDeclaration = NO;
177               hasEmbeddedCSS = NO;
178             }
179         }
180       else
181         {
182           if (*(char *) currentChar == '{')
183             inCSSDeclaration = YES;
184           if (*(char *) currentChar == ',')
185             hasEmbeddedCSS = NO;
186           else if (!hasEmbeddedCSS)
187             {
188               if (*(char *) currentChar == '@')
189                 hasEmbeddedCSS = YES;
190               else
191                 if (*(char *) currentChar > 32)
192                   {
193                     [css appendString: [NSString stringWithCharacters: start
194                                                  length: (currentChar - start)]];
195                     [css appendString: @".SOGoHTMLMail-CSS-Delimiter "];
196                     hasEmbeddedCSS = YES;
197                     start = currentChar;
198                   }
199             }
200         }
201     }
202   [css appendString: [NSString stringWithCharacters: start
203                                length: (currentChar - start)]];
204 }
205
206 - (void) startElement: (NSString *) _localName
207             namespace: (NSString *) _ns
208               rawName: (NSString *) _rawName
209            attributes: (id <SaxAttributes>) _attributes
210 {
211   unsigned int count, max;
212   NSString *name, *value, *cid;
213   NSMutableString *resultPart;
214   BOOL skipAttribute;
215
216   showWhoWeAre();
217   if (inStyle || inScript)
218     ;
219   else if ([_localName caseInsensitiveCompare: @"body"] == NSOrderedSame)
220     inBody = YES;
221   else if ([_localName caseInsensitiveCompare: @"script"] == NSOrderedSame)
222     inScript = YES;
223   else if ([_localName caseInsensitiveCompare: @"style"] == NSOrderedSame)
224     inStyle = YES;
225   else if (inBody)
226     {
227       resultPart = [NSMutableString new];
228       [resultPart appendFormat: @"<%@", _rawName];
229       
230       max = [_attributes count];
231       for (count = 0; count < max; count++)
232         {
233           skipAttribute = NO;
234           name = [_attributes nameAtIndex: count];
235           if ([[name lowercaseString] hasPrefix: @"on"])
236             skipAttribute = YES;
237           else if ([name caseInsensitiveCompare: @"src"] == NSOrderedSame)
238             {
239               value = [_attributes valueAtIndex: count];
240               if ([value hasPrefix: @"cid:"])
241                 {
242                   cid = [NSString stringWithFormat: @"<%@>",
243                                   [value substringFromIndex: 4]];
244                   value = [attachmentIds objectForKey: cid];
245                   skipAttribute = (value == nil);
246                 }
247               else
248                 skipAttribute = YES;
249             }
250           else
251             value = [_attributes valueAtIndex: count];
252           if (!skipAttribute)
253             [resultPart appendFormat: @" %@=\"%@\"",
254                         name, [value stringByReplacingString: @"\""
255                                      withString: @"\\\""]];
256         }
257
258       [resultPart appendString: @">"];
259       [result appendString: resultPart];
260     }
261 }
262
263 - (void) _finishCSS
264 {
265   [css replaceString: @".SOGoHTMLMail-CSS-Delimiter body"
266        withString: @".SOGoHTMLMail-CSS-Delimiter"];
267   [css replaceString: @";" withString: @" !important;"];
268   [css replaceString: @"<!--" withString: @""];
269   [css replaceString: @"-->" withString: @""];
270 }
271
272 - (void) endElement: (NSString *) _localName
273           namespace: (NSString *) _ns
274             rawName: (NSString *) _rawName
275 {
276   showWhoWeAre();
277
278   if (inStyle)
279     {
280      if ([_localName caseInsensitiveCompare: @"style"] == NSOrderedSame)
281        {
282          inStyle = NO;
283          inCSSDeclaration = NO;
284        }
285     }
286   else if (inScript)
287     inScript = ([_localName caseInsensitiveCompare: @"script"] != NSOrderedSame);
288   else if (inBody)
289     {
290       if ([_localName caseInsensitiveCompare: @"body"] == NSOrderedSame)
291         {
292           inBody = NO;
293           if (css)
294             [self _finishCSS];
295         }
296       else
297         [result appendFormat: @"</%@>", _localName];
298     }
299 }
300
301 - (void) characters: (unichar *) _chars
302              length: (int) _len
303 {
304   showWhoWeAre();
305   if (!inScript)
306     {
307       if (inStyle)
308         [self _appendStyle: _chars length: _len];
309       else if (inBody)
310         {
311           NSString *tmpString;
312   
313           tmpString = [NSString stringWithCharacters: _chars length: _len];
314           
315           // HACK: This is to avoid appending the useless junk in the <html> tag
316           //       that Outlook adds. It seems to confuse the XML parser for
317           //       forwarded messages as we get this in the _body_ of the email
318           //       while we really aren't in it!
319           if (![tmpString hasPrefix: @" xmlns:v=\"urn:schemas-microsoft-com:vml\""])
320             [result appendString: [tmpString stringByEscapingHTMLString]];
321         }
322     }
323 }
324
325 - (void) ignorableWhitespace: (unichar *) _chars
326                       length: (int) _len
327 {
328   showWhoWeAre();
329 }
330
331 - (void) processingInstruction: (NSString *) _pi
332                           data: (NSString *) _data
333 {
334   showWhoWeAre();
335 }
336
337 - (void) setDocumentLocator: (id <NSObject, SaxLocator>) _locator
338 {
339   showWhoWeAre();
340 }
341
342 - (void) skippedEntity: (NSString *) _entityName
343 {
344   showWhoWeAre();
345 }
346
347 /* SaxLexicalHandler */
348 - (void) comment: (unichar *) _chars
349           length: (int) _len
350 {
351   showWhoWeAre();
352   if (inStyle)
353     [self _appendStyle: _chars length: _len];
354 }
355
356 - (void) startDTD: (NSString *) _name
357          publicId: (NSString *) _pub
358          systemId: (NSString *) _sys
359 {
360   showWhoWeAre();
361 }
362
363 - (void) endDTD
364 {
365   showWhoWeAre();
366 }
367
368 - (void) startEntity: (NSString *) _name
369 {
370   showWhoWeAre();
371 }
372
373 - (void) endEntity: (NSString *) _name
374 {
375   showWhoWeAre();
376 }
377
378 - (void) startCDATA
379 {
380   showWhoWeAre();
381 }
382
383 - (void) endCDATA
384 {
385   showWhoWeAre();
386 }
387
388 @end
389
390 @interface NSDictionary (SOGoDebug)
391
392 - (void) dump;
393
394 @end
395
396 @implementation NSDictionary (SOGoDebug)
397
398 - (void) dump
399 {
400   NSEnumerator *keys;
401   NSString *key;
402   NSMutableString *dump;
403
404   dump = [NSMutableString new];
405   [dump appendFormat: @"\nNSDictionary dump (%@):\n", self];
406   keys = [[self allKeys] objectEnumerator];
407   key = [keys nextObject];
408   while (key)
409     {
410       [dump appendFormat: @"%@: %@\n", key, [self objectForKey: key]];
411       key = [keys nextObject];
412     }
413   [dump appendFormat: @"--- end ---\n"];
414
415   NSLog (dump);
416   [dump release];
417 }
418
419 @end
420
421 @implementation UIxMailPartHTMLViewer
422
423 - (id) init
424 {
425   if ((self = [super init]))
426     {
427       handler = nil;
428     }
429
430   return self;
431 }
432
433 - (void) dealloc
434 {
435   [handler release];
436   [super dealloc];
437 }
438
439 - (xmlCharEncoding) _xmlCharsetForCharset: (NSString *) charset
440 {
441   struct { NSString *name; xmlCharEncoding encoding; } xmlEncodings[] = {
442     { @"us-ascii", XML_CHAR_ENCODING_ASCII},
443     { @"utf-8", XML_CHAR_ENCODING_UTF8},
444     { @"utf-16le", XML_CHAR_ENCODING_UTF16LE},
445     { @"utf-16be",  XML_CHAR_ENCODING_UTF16BE},
446     { @"ucs-4le", XML_CHAR_ENCODING_UCS4LE},
447     { @"ucs-4be", XML_CHAR_ENCODING_UCS4BE},
448     { @"ebcdic", XML_CHAR_ENCODING_EBCDIC},
449 //     { @"iso-10646" , XML_CHAR_ENCODING_UCS4_2143},
450 //     {  , XML_CHAR_ENCODING_UCS4_3412},
451 //     { @"ucs-2", XML_CHAR_ENCODING_UCS2},
452     { @"iso8859_1", XML_CHAR_ENCODING_8859_1},
453     { @"iso-8859-1", XML_CHAR_ENCODING_8859_1},
454     { @"iso-8859-2",  XML_CHAR_ENCODING_8859_2},
455     { @"iso-8859-3", XML_CHAR_ENCODING_8859_3},
456     { @"iso-8859-4", XML_CHAR_ENCODING_8859_4},
457     { @"iso-8859-5", XML_CHAR_ENCODING_8859_5},
458     { @"iso-8859-6", XML_CHAR_ENCODING_8859_6},
459     { @"iso-8859-7", XML_CHAR_ENCODING_8859_7},
460     { @"iso-8859-8", XML_CHAR_ENCODING_8859_8},
461     { @"iso-8859-9", XML_CHAR_ENCODING_8859_9},
462     { @"iso-2022-jp", XML_CHAR_ENCODING_2022_JP},
463 //     { @"iso-2022-jp", XML_CHAR_ENCODING_SHIFT_JIS},
464     { @"euc-jp", XML_CHAR_ENCODING_EUC_JP}};
465   unsigned count;
466   xmlCharEncoding encoding;
467
468   encoding = XML_CHAR_ENCODING_NONE;
469   count = 0;
470
471   while (encoding == XML_CHAR_ENCODING_NONE
472          && count < (sizeof (xmlEncodings) / sizeof (xmlEncodings[0])))
473     if ([charset isEqualToString: xmlEncodings[count].name])
474       encoding = xmlEncodings[count].encoding;
475     else
476       count++;
477
478   if (encoding == XML_CHAR_ENCODING_NONE)
479     encoding = XML_CHAR_ENCODING_8859_1;
480
481   return encoding;
482 }
483
484 - (xmlCharEncoding) _xmlCharEncoding
485 {
486
487   NSString *charset;
488
489   charset = [[bodyInfo objectForKey:@"parameterList"]
490               objectForKey: @"charset"];
491   if (![charset length])
492     charset = @"us-ascii";
493   
494   return [self _xmlCharsetForCharset: [charset lowercaseString]];
495 }
496
497 - (void) _parseContent
498 {
499   NSObject <SaxXMLReader> *parser;
500   NSData *preparsedContent;
501   SOGoMailObject *mail;
502
503   mail = [self clientObject];
504
505   preparsedContent = [super decodedFlatContent];
506   parser = [[SaxXMLReaderFactory standardXMLReaderFactory]
507              createXMLReaderForMimeType: @"text/html"];
508
509   handler = [_UIxHTMLMailContentHandler new];
510   [handler setAttachmentIds: [mail fetchAttachmentIds]];
511   [handler setContentEncoding: [self _xmlCharEncoding]];
512   [parser setContentHandler: handler];
513   [parser parseFromSource: preparsedContent];
514 }
515
516 - (NSString *) cssContent
517 {
518   NSString *cssContent, *css;
519
520   if (!handler)
521     [self _parseContent];
522
523   css = [handler css];
524   if ([css length])
525     cssContent
526       = [NSString stringWithFormat: @"<style type=\"text/css\">%@</style>",
527                   [handler css]];
528   else
529     cssContent = @"";
530
531   return cssContent;
532 }
533
534 - (NSString *) flatContentAsString
535 {
536   if (!handler)
537     [self _parseContent];
538
539   return [handler result];
540 }
541
542 @end