]> err.no Git - scalable-opengroupware.org/blob - SoObjects/Mailer/SOGoMailBodyPart.m
fd4ebf41d5faae4e762020e9f9bf0aae9c014455
[scalable-opengroupware.org] / SoObjects / Mailer / SOGoMailBodyPart.m
1 /*
2   Copyright (C) 2004-2005 SKYRIX Software AG
3
4   This file is part of OpenGroupware.org.
5
6   OGo is free software; you can redistribute it and/or modify it under
7   the terms of the GNU Lesser General Public License as published by the
8   Free Software Foundation; either version 2, or (at your option) any
9   later version.
10
11   OGo is distributed in the hope that it will be useful, but WITHOUT ANY
12   WARRANTY; without even the implied warranty of MERCHANTABILITY or
13   FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public
14   License for more details.
15
16   You should have received a copy of the GNU Lesser General Public
17   License along with OGo; see the file COPYING.  If not, write to the
18   Free Software Foundation, 59 Temple Place - Suite 330, Boston, MA
19   02111-1307, USA.
20 */
21
22 #import <Foundation/NSArray.h>
23 #import <Foundation/NSDictionary.h>
24 #import <Foundation/NSEnumerator.h>
25 #import <Foundation/NSString.h>
26 #import <Foundation/NSUserDefaults.h>
27
28 #import <NGObjWeb/NSException+HTTP.h>
29 #import <NGObjWeb/SoObject+SoDAV.h>
30 #import <NGObjWeb/WOContext.h>
31 #import <NGObjWeb/WOResponse.h>
32 #import <NGExtensions/NSNull+misc.h>
33 #import <NGExtensions/NSObject+Logs.h>
34 #import <NGExtensions/NGBase64Coding.h>
35 #import <NGImap4/NGImap4Connection.h>
36
37 #import <SoObjects/SOGo/NSDictionary+Utilities.h>
38
39 #import "SOGoMailObject.h"
40 #import "SOGoMailManager.h"
41
42 #import "SOGoMailBodyPart.h"
43
44 @implementation SOGoMailBodyPart
45
46 static NSString *mailETag = nil;
47 static BOOL debugOn = NO;
48
49 + (void) initialize
50 {
51   NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
52   
53   if (![[ud objectForKey:@"SOGoMailDisableETag"] boolValue]) {
54     mailETag = [[NSString alloc] initWithFormat:@"\"imap4url_%d_%d_%03d\"",
55                                  UIX_MAILER_MAJOR_VERSION,
56                                  UIX_MAILER_MINOR_VERSION,
57                                  UIX_MAILER_SUBMINOR_VERSION];
58     NSLog(@"Note(SOGoMailBodyPart): using constant etag for mail parts: '%@'", 
59           mailETag);
60   }
61   else
62     NSLog(@"Note(SOGoMailBodyPart): etag caching disabled!");
63 }
64
65 - (void)dealloc {
66   [self->partInfo   release];
67   [self->identifier release];
68   [self->pathToPart release];
69   [super dealloc];
70 }
71
72 /* hierarchy */
73
74 - (SOGoMailObject *)mailObject {
75   return [[self container] mailObject];
76 }
77
78 /* IMAP4 */
79
80 - (NSString *)bodyPartName {
81   NSString *s;
82   NSRange  r;
83
84   s = [self nameInContainer];
85   r = [s rangeOfString:@"."]; /* strip extensions */
86   if (r.length == 0)
87     return s;
88   return [s substringToIndex:r.location];
89 }
90
91 - (NSArray *)bodyPartPath {
92   NSMutableArray *p;
93   id obj;
94   
95   if (self->pathToPart != nil)
96     return [self->pathToPart isNotNull] ? self->pathToPart : nil;
97   
98   p = [[NSMutableArray alloc] initWithCapacity:8];
99   for (obj = self; [obj isKindOfClass:[SOGoMailBodyPart class]]; 
100        obj = [obj container]) {
101     [p insertObject:[obj bodyPartName] atIndex:0];
102   }
103   
104   self->pathToPart = [p copy];
105   [p release];
106   return self->pathToPart;
107 }
108
109 - (NSString *)bodyPartIdentifier {
110   if (self->identifier != nil)
111     return [self->identifier isNotNull] ? self->identifier : nil;
112   
113   self->identifier =
114     [[[self bodyPartPath] componentsJoinedByString:@"."] copy];
115   return self->identifier;
116 }
117
118 - (NSURL *)imap4URL {
119   /* reuse URL of message */
120   return [[self mailObject] imap4URL];
121 }
122
123 /* part info */
124
125 - (id)partInfo {
126   if (self->partInfo != nil)
127     return [self->partInfo isNotNull] ? self->partInfo : nil;
128
129   self->partInfo =
130     [[[self mailObject] lookupInfoForBodyPart:[self bodyPartPath]] retain];
131   return self->partInfo;
132 }
133
134 /* name lookup */
135
136 - (id) lookupImap4BodyPartKey: (NSString *) _key
137                     inContext: (id) _ctx
138 {
139   // TODO: we might want to check for existence prior controller creation
140   Class clazz;
141   
142   clazz = [SOGoMailBodyPart bodyPartClassForKey:_key inContext:_ctx];
143
144   return [clazz objectWithName: _key inContainer: self];
145 }
146
147 - (id) lookupName: (NSString *) _key
148         inContext: (id) _ctx
149           acquire: (BOOL) _flag
150 {
151   id obj;
152   
153   /* first check attributes directly bound to the application */
154   obj = [super lookupName:_key inContext:_ctx acquire:NO];
155   if (!obj)
156     {
157       /* lookup body part */
158       if ([self isBodyPartKey:_key inContext:_ctx])
159         obj = [self lookupImap4BodyPartKey:_key inContext:_ctx];
160       /* should check whether such a filename exist in the attached names */
161       if (!obj)
162         obj = self;
163     }
164
165   return obj;      
166 }
167
168 /* fetch */
169
170 - (NSData *) fetchBLOB
171 {
172   // HEADER, HEADER.FIELDS, HEADER.FIELDS.NOT, MIME, TEXT
173   NSString *enc;
174   NSData *data;
175   
176   data = [[self imap4Connection] fetchContentOfBodyPart:
177                                    [self bodyPartIdentifier]
178                                  atURL:[self imap4URL]];
179   if (data == nil) return nil;
180
181   /* check for content encodings */
182   enc = [[self partInfo] valueForKey: @"encoding"];
183  
184   /* if we haven't found one, check out the main message's encoding
185      as we could be trying to fetch the message's content as a part */
186   if (!enc)
187     enc = [[[[self mailObject] fetchCoreInfos] valueForKey: @"body"]
188             valueForKey: @"encoding"];
189
190   if (enc)
191     {
192       enc = [enc uppercaseString];
193       
194       if ([enc isEqualToString:@"BASE64"])
195         data = [data dataByDecodingBase64];
196       else if ([enc isEqualToString:@"7BIT"])
197         ; /* keep data as is */ // TODO: do we need to change encodings?
198       else
199         [self errorWithFormat:@"unsupported encoding: %@", enc];
200     }
201   
202   return data;
203 }
204
205 /* WebDAV */
206
207 - (NSString *)contentTypeForBodyPartInfo:(id)_info {
208   NSMutableString *type;
209   NSString     *mt, *st;
210   NSDictionary *parameters;
211   NSEnumerator *ke;
212   NSString     *pn;
213     
214   if (![_info isNotNull])
215     return nil;
216   
217   mt = [_info valueForKey:@"type"];    if (![mt isNotNull]) return nil;
218   st = [_info valueForKey:@"subtype"]; if (![st isNotNull]) return nil;
219   
220   type = [NSMutableString stringWithCapacity:16];
221   [type appendString:[mt lowercaseString]];
222   [type appendString:@"/"];
223   [type appendString:[st lowercaseString]];
224   
225   parameters = [_info valueForKey:@"parameterList"];
226   ke = [parameters keyEnumerator];
227   while ((pn = [ke nextObject]) != nil) {
228     [type appendString:@"; "];
229     [type appendString:pn];
230     [type appendString:@"=\""];
231     [type appendString:[[parameters objectForKey:pn] stringValue]];
232     [type appendString:@"\""];
233   }
234   return type;
235 }
236
237 - (NSString *) contentTypeForPathExtension: (NSString *) pe
238 {
239   if ([pe length] == 0)
240     return @"application/octet-stream";
241   
242   /* TODO: add some map */
243   if ([pe isEqualToString:@"gif"]) return @"image/gif";
244   if ([pe isEqualToString:@"png"]) return @"image/png";
245   if ([pe isEqualToString:@"jpg"]) return @"image/jpeg";
246   if ([pe isEqualToString:@"txt"]) return @"text/plain";
247   
248   return @"application/octet-stream";
249 }
250
251 - (NSString *) davContentType
252 {
253   // TODO: what about the content-type and other headers?
254   //       => we could pass them in as the extension? (eg generate 1.gif!)
255   NSString *parts, *contentType, *extension;
256   
257   /* try type from body structure info */
258   
259   parts = [self contentTypeForBodyPartInfo: [self partInfo]];
260   contentType = [[parts componentsSeparatedByString: @";"] objectAtIndex: 0];
261
262   if (![contentType length])
263     {
264       extension = [[self nameInContainer] pathExtension];
265       contentType = [self contentTypeForPathExtension: extension];
266     }
267
268   return contentType;
269 }
270
271 /* actions */
272
273 - (id)GETAction:(id)_ctx {
274   NSException *error;
275   WOResponse *r;
276   NSData     *data;
277   NSString   *etag, *mimeType;
278   
279   if ((error = [self matchesRequestConditionInContext:_ctx]) != nil) {
280     // TODO: currently we fetch the body structure to get here - check this!
281     /* check whether the mail still exists */
282     if (![[self mailObject] doesMailExist]) {
283       return [NSException exceptionWithHTTPStatus:404 /* Not Found */
284                           reason:@"mail was deleted"];
285     }
286     return error; /* return 304 or 416 */
287   }
288
289   [self debugWithFormat:@"should fetch body part: %@", 
290           [self bodyPartIdentifier]];
291   
292   if ((data = [self fetchBLOB]) == nil) {
293     return [NSException exceptionWithHTTPStatus:404 /* not found */
294                         reason:@"did not find body part"];
295   }
296   
297   [self debugWithFormat:@"  fetched %d bytes: %@", [data length],
298         [self partInfo]];
299   
300   // TODO: wrong, could be encoded
301   r = [(WOContext *)_ctx response];
302   mimeType = [self davContentType];
303   if ([mimeType isEqualToString: @"application/x-xpinstall"])
304     mimeType = @"application/octet-stream";
305
306   [r setHeader: mimeType forKey:@"content-type"];
307   [r setHeader: [NSString stringWithFormat:@"%d", [data length]]
308      forKey: @"content-length"];
309
310   if ((etag = [self davEntityTag]) != nil)
311     [r setHeader:etag forKey:@"etag"];
312
313   [r setContent:data];
314
315   return r;
316 }
317
318 /* factory */
319
320 + (Class) bodyPartClassForKey: (NSString *) _key
321                     inContext: (id) _ctx
322 {
323   NSString *pe;
324   
325   pe = [_key pathExtension];
326   if (![pe isNotNull] || [pe length] == 0)
327     return self;
328   
329   /* hard coded for now */
330   
331   switch ([pe length]) {
332   case 3:
333     if ([pe isEqualToString:@"gif"] ||
334         [pe isEqualToString:@"png"] ||
335         [pe isEqualToString:@"jpg"])
336       return NSClassFromString(@"SOGoImageMailBodyPart");
337     if ([pe isEqualToString:@"ics"])
338       return NSClassFromString(@"SOGoCalendarMailBodyPart");
339     if ([pe isEqualToString:@"vcf"])
340       return NSClassFromString(@"SOGoVCardMailBodyPart");
341     break;
342   case 4:
343     if ([pe isEqualToString:@"mail"])
344       return NSClassFromString(@"SOGoMessageMailBodyPart");
345     break;
346   default:
347     return self;
348   }
349   return self;
350 }
351
352 + (Class) bodyPartClassForMimeType: (NSString *) mimeType
353                          inContext: (id) _ctx
354 {
355   NSString *classString;
356   Class klazz;
357
358   if ([mimeType isEqualToString: @"image/gif"]
359       || [mimeType isEqualToString: @"image/png"]
360       || [mimeType isEqualToString: @"image/jpg"]
361       || [mimeType isEqualToString: @"image/jpeg"])
362     classString = @"SOGoImageMailBodyPart";
363   else if ([mimeType isEqualToString: @"text/calendar"])
364     classString = @"SOGoCalendarMailBodyPart";
365   else if ([mimeType isEqualToString: @"text/x-vcard"])
366     classString = @"SOGoVCardMailBodyPart";
367   else if ([mimeType isEqualToString: @"message/rfc822"])
368     classString = @"SOGoMessageMailBodyPart";
369   else
370     {
371       classString = @"SOGoMailBodyPart";
372 //       NSLog (@"unhandled mime type: '%@'", mimeType);
373     }
374
375   klazz = NSClassFromString (classString);
376
377   return klazz;
378 }
379
380 /* etag support */
381
382 - (id)davEntityTag {
383   return mailETag;
384 }
385
386 /* debugging */
387
388 - (BOOL)isDebuggingEnabled {
389   return debugOn;
390 }
391
392 @end /* SOGoMailBodyPart */