]> err.no Git - scalable-opengroupware.org/blob - SoObjects/Mailer/SOGoMailBodyPart.m
c60f34b9b179c6d325a2d8f64748796039081a87
[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 /* We overwrite the super's class method in order to make sure
148    we aren't dealing with our actual filename as the _key. That
149    could lead to problems if we weren't doing this as our filename
150    could start with a digit, leading to a wrong assumption in
151    the super class
152 */
153 - (BOOL)isBodyPartKey:(NSString *)_key inContext:(id)_ctx
154 {
155   NSString *s;
156   
157   s = [[[self partInfo] objectForKey: @"parameterList"] objectForKey: @"name"];
158
159   if (!s)
160     s = [[[[self partInfo] objectForKey: @"disposition"] objectForKey: @"parameterList"] objectForKey: @"filename"];
161   
162   if (s && [s isEqualToString: _key]) return NO;
163
164   return [super isBodyPartKey: _key  inContext: _ctx];
165 }
166
167 - (id) lookupName: (NSString *) _key
168         inContext: (id) _ctx
169           acquire: (BOOL) _flag
170 {
171   id obj;
172   
173   /* first check attributes directly bound to the application */
174   obj = [super lookupName:_key inContext:_ctx acquire:NO];
175   if (!obj)
176     {
177       /* lookup body part */
178       if ([self isBodyPartKey:_key inContext:_ctx])
179         obj = [self lookupImap4BodyPartKey:_key inContext:_ctx];
180       /* should check whether such a filename exist in the attached names */
181       if (!obj)
182         obj = self;
183     }
184
185   return obj;      
186 }
187
188 /* fetch */
189
190 - (NSData *) fetchBLOB
191 {
192   // HEADER, HEADER.FIELDS, HEADER.FIELDS.NOT, MIME, TEXT
193   NSString *enc;
194   NSData *data;
195   
196   data = [[self imap4Connection] fetchContentOfBodyPart:
197                                    [self bodyPartIdentifier]
198                                  atURL:[self imap4URL]];
199   if (data == nil) return nil;
200
201   /* check for content encodings */
202   enc = [[self partInfo] valueForKey: @"encoding"];
203  
204   /* if we haven't found one, check out the main message's encoding
205      as we could be trying to fetch the message's content as a part */
206   if (!enc)
207     enc = [[[[self mailObject] fetchCoreInfos] valueForKey: @"body"]
208             valueForKey: @"encoding"];
209
210   if (enc)
211     {
212       enc = [enc uppercaseString];
213       
214       if ([enc isEqualToString:@"BASE64"])
215         data = [data dataByDecodingBase64];
216       else if ([enc isEqualToString:@"7BIT"])
217         ; /* keep data as is */ // TODO: do we need to change encodings?
218       else
219         [self errorWithFormat:@"unsupported encoding: %@", enc];
220     }
221   
222   return data;
223 }
224
225 /* WebDAV */
226
227 - (NSString *)contentTypeForBodyPartInfo:(id)_info {
228   NSMutableString *type;
229   NSString     *mt, *st;
230   NSDictionary *parameters;
231   NSEnumerator *ke;
232   NSString     *pn;
233     
234   if (![_info isNotNull])
235     return nil;
236   
237   mt = [_info valueForKey:@"type"];    if (![mt isNotNull]) return nil;
238   st = [_info valueForKey:@"subtype"]; if (![st isNotNull]) return nil;
239   
240   type = [NSMutableString stringWithCapacity:16];
241   [type appendString:[mt lowercaseString]];
242   [type appendString:@"/"];
243   [type appendString:[st lowercaseString]];
244   
245   parameters = [_info valueForKey:@"parameterList"];
246   ke = [parameters keyEnumerator];
247   while ((pn = [ke nextObject]) != nil) {
248     [type appendString:@"; "];
249     [type appendString:pn];
250     [type appendString:@"=\""];
251     [type appendString:[[parameters objectForKey:pn] stringValue]];
252     [type appendString:@"\""];
253   }
254   return type;
255 }
256
257 - (NSString *) contentTypeForPathExtension: (NSString *) pe
258 {
259   if ([pe length] == 0)
260     return @"application/octet-stream";
261   
262   /* TODO: add some map */
263   if ([pe isEqualToString:@"gif"]) return @"image/gif";
264   if ([pe isEqualToString:@"png"]) return @"image/png";
265   if ([pe isEqualToString:@"jpg"]) return @"image/jpeg";
266   if ([pe isEqualToString:@"txt"]) return @"text/plain";
267   
268   return @"application/octet-stream";
269 }
270
271 - (NSString *) davContentType
272 {
273   // TODO: what about the content-type and other headers?
274   //       => we could pass them in as the extension? (eg generate 1.gif!)
275   NSString *parts, *contentType, *extension;
276   
277   /* try type from body structure info */
278   
279   parts = [self contentTypeForBodyPartInfo: [self partInfo]];
280   contentType = [[parts componentsSeparatedByString: @";"] objectAtIndex: 0];
281
282   if (![contentType length])
283     {
284       extension = [[self nameInContainer] pathExtension];
285       contentType = [self contentTypeForPathExtension: extension];
286     }
287
288   return contentType;
289 }
290
291 /* actions */
292
293 - (id)GETAction:(id)_ctx {
294   NSException *error;
295   WOResponse *r;
296   NSData     *data;
297   NSString   *etag, *mimeType;
298   
299   if ((error = [self matchesRequestConditionInContext:_ctx]) != nil) {
300     // TODO: currently we fetch the body structure to get here - check this!
301     /* check whether the mail still exists */
302     if (![[self mailObject] doesMailExist]) {
303       return [NSException exceptionWithHTTPStatus:404 /* Not Found */
304                           reason:@"mail was deleted"];
305     }
306     return error; /* return 304 or 416 */
307   }
308
309   [self debugWithFormat:@"should fetch body part: %@", 
310           [self bodyPartIdentifier]];
311   
312   if ((data = [self fetchBLOB]) == nil) {
313     return [NSException exceptionWithHTTPStatus:404 /* not found */
314                         reason:@"did not find body part"];
315   }
316   
317   [self debugWithFormat:@"  fetched %d bytes: %@", [data length],
318         [self partInfo]];
319   
320   // TODO: wrong, could be encoded
321   r = [(WOContext *)_ctx response];
322   mimeType = [self davContentType];
323   if ([mimeType isEqualToString: @"application/x-xpinstall"])
324     mimeType = @"application/octet-stream";
325
326   [r setHeader: mimeType forKey:@"content-type"];
327   [r setHeader: [NSString stringWithFormat:@"%d", [data length]]
328      forKey: @"content-length"];
329
330   if ((etag = [self davEntityTag]) != nil)
331     [r setHeader:etag forKey:@"etag"];
332
333   [r setContent:data];
334
335   return r;
336 }
337
338 /* factory */
339
340 + (Class) bodyPartClassForKey: (NSString *) _key
341                     inContext: (id) _ctx
342 {
343   NSString *pe;
344   
345   pe = [_key pathExtension];
346   if (![pe isNotNull] || [pe length] == 0)
347     return self;
348   
349   /* hard coded for now */
350   
351   switch ([pe length]) {
352   case 3:
353     if ([pe isEqualToString:@"gif"] ||
354         [pe isEqualToString:@"png"] ||
355         [pe isEqualToString:@"jpg"])
356       return NSClassFromString(@"SOGoImageMailBodyPart");
357     if ([pe isEqualToString:@"ics"])
358       return NSClassFromString(@"SOGoCalendarMailBodyPart");
359     if ([pe isEqualToString:@"vcf"])
360       return NSClassFromString(@"SOGoVCardMailBodyPart");
361     break;
362   case 4:
363     if ([pe isEqualToString:@"mail"])
364       return NSClassFromString(@"SOGoMessageMailBodyPart");
365     break;
366   default:
367     return self;
368   }
369   return self;
370 }
371
372 + (Class) bodyPartClassForMimeType: (NSString *) mimeType
373                          inContext: (id) _ctx
374 {
375   NSString *classString;
376   Class klazz;
377
378   if ([mimeType isEqualToString: @"image/gif"]
379       || [mimeType isEqualToString: @"image/png"]
380       || [mimeType isEqualToString: @"image/jpg"]
381       || [mimeType isEqualToString: @"image/jpeg"])
382     classString = @"SOGoImageMailBodyPart";
383   else if ([mimeType isEqualToString: @"text/calendar"]
384            || [mimeType isEqualToString: @"application/ics"])
385     classString = @"SOGoCalendarMailBodyPart";
386   else if ([mimeType isEqualToString: @"text/x-vcard"])
387     classString = @"SOGoVCardMailBodyPart";
388   else if ([mimeType isEqualToString: @"message/rfc822"])
389     classString = @"SOGoMessageMailBodyPart";
390   else
391     {
392       classString = @"SOGoMailBodyPart";
393 //       NSLog (@"unhandled mime type: '%@'", mimeType);
394     }
395
396   klazz = NSClassFromString (classString);
397
398   return klazz;
399 }
400
401 /* etag support */
402
403 - (id)davEntityTag {
404   return mailETag;
405 }
406
407 /* debugging */
408
409 - (BOOL)isDebuggingEnabled {
410   return debugOn;
411 }
412
413 @end /* SOGoMailBodyPart */