]> err.no Git - scalable-opengroupware.org/blob - SoObjects/Mailer/SOGoMailBodyPart.m
git-svn-id: http://svn.opengroupware.org/SOGo/inverse/trunk@1265 d1b88da0-ebda-0310...
[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   
183   if ((enc = [[self partInfo] valueForKey:@"encoding"]) != nil) {
184     enc = [enc uppercaseString];
185     
186     if ([enc isEqualToString:@"BASE64"])
187       data = [data dataByDecodingBase64];
188     else if ([enc isEqualToString:@"7BIT"])
189       ; /* keep data as is */ // TODO: do we need to change encodings?
190     else
191       [self errorWithFormat:@"unsupported encoding: %@", enc];
192   }
193   
194   return data;
195 }
196
197 /* WebDAV */
198
199 - (NSString *)contentTypeForBodyPartInfo:(id)_info {
200   NSMutableString *type;
201   NSString     *mt, *st;
202   NSDictionary *parameters;
203   NSEnumerator *ke;
204   NSString     *pn;
205     
206   if (![_info isNotNull])
207     return nil;
208   
209   mt = [_info valueForKey:@"type"];    if (![mt isNotNull]) return nil;
210   st = [_info valueForKey:@"subtype"]; if (![st isNotNull]) return nil;
211   
212   type = [NSMutableString stringWithCapacity:16];
213   [type appendString:[mt lowercaseString]];
214   [type appendString:@"/"];
215   [type appendString:[st lowercaseString]];
216   
217   parameters = [_info valueForKey:@"parameterList"];
218   ke = [parameters keyEnumerator];
219   while ((pn = [ke nextObject]) != nil) {
220     [type appendString:@"; "];
221     [type appendString:pn];
222     [type appendString:@"=\""];
223     [type appendString:[[parameters objectForKey:pn] stringValue]];
224     [type appendString:@"\""];
225   }
226   return type;
227 }
228
229 - (NSString *) contentTypeForPathExtension: (NSString *) pe
230 {
231   if ([pe length] == 0)
232     return @"application/octet-stream";
233   
234   /* TODO: add some map */
235   if ([pe isEqualToString:@"gif"]) return @"image/gif";
236   if ([pe isEqualToString:@"png"]) return @"image/png";
237   if ([pe isEqualToString:@"jpg"]) return @"image/jpeg";
238   if ([pe isEqualToString:@"txt"]) return @"text/plain";
239   
240   return @"application/octet-stream";
241 }
242
243 - (NSString *) davContentType
244 {
245   // TODO: what about the content-type and other headers?
246   //       => we could pass them in as the extension? (eg generate 1.gif!)
247   NSString *parts, *contentType, *extension;
248   
249   /* try type from body structure info */
250   
251   parts = [self contentTypeForBodyPartInfo: [self partInfo]];
252   contentType = [[parts componentsSeparatedByString: @";"] objectAtIndex: 0];
253
254   if (![contentType length])
255     {
256       extension = [[self nameInContainer] pathExtension];
257       contentType = [self contentTypeForPathExtension: extension];
258     }
259
260   return contentType;
261 }
262
263 /* actions */
264
265 - (id)GETAction:(id)_ctx {
266   NSException *error;
267   WOResponse *r;
268   NSData     *data;
269   NSString   *etag, *mimeType;
270   
271   if ((error = [self matchesRequestConditionInContext:_ctx]) != nil) {
272     // TODO: currently we fetch the body structure to get here - check this!
273     /* check whether the mail still exists */
274     if (![[self mailObject] doesMailExist]) {
275       return [NSException exceptionWithHTTPStatus:404 /* Not Found */
276                           reason:@"mail was deleted"];
277     }
278     return error; /* return 304 or 416 */
279   }
280
281   [self debugWithFormat:@"should fetch body part: %@", 
282           [self bodyPartIdentifier]];
283   
284   if ((data = [self fetchBLOB]) == nil) {
285     return [NSException exceptionWithHTTPStatus:404 /* not found */
286                         reason:@"did not find body part"];
287   }
288   
289   [self debugWithFormat:@"  fetched %d bytes: %@", [data length],
290         [self partInfo]];
291   
292   // TODO: wrong, could be encoded
293   r = [(WOContext *)_ctx response];
294   mimeType = [self davContentType];
295   if ([mimeType isEqualToString: @"application/x-xpinstall"])
296     mimeType = @"application/octet-stream";
297
298   [r setHeader: mimeType forKey:@"content-type"];
299   [r setHeader: [NSString stringWithFormat:@"%d", [data length]]
300      forKey: @"content-length"];
301
302   if ((etag = [self davEntityTag]) != nil)
303     [r setHeader:etag forKey:@"etag"];
304
305   [r setContent:data];
306
307   return r;
308 }
309
310 /* factory */
311
312 + (Class) bodyPartClassForKey: (NSString *) _key
313                     inContext: (id) _ctx
314 {
315   NSString *pe;
316   
317   pe = [_key pathExtension];
318   if (![pe isNotNull] || [pe length] == 0)
319     return self;
320   
321   /* hard coded for now */
322   
323   switch ([pe length]) {
324   case 3:
325     if ([pe isEqualToString:@"gif"] ||
326         [pe isEqualToString:@"png"] ||
327         [pe isEqualToString:@"jpg"])
328       return NSClassFromString(@"SOGoImageMailBodyPart");
329     if ([pe isEqualToString:@"ics"])
330       return NSClassFromString(@"SOGoCalendarMailBodyPart");
331     if ([pe isEqualToString:@"vcf"])
332       return NSClassFromString(@"SOGoVCardMailBodyPart");
333     break;
334   case 4:
335     if ([pe isEqualToString:@"mail"])
336       return NSClassFromString(@"SOGoMessageMailBodyPart");
337     break;
338   default:
339     return self;
340   }
341   return self;
342 }
343
344 + (Class) bodyPartClassForMimeType: (NSString *) mimeType
345                          inContext: (id) _ctx
346 {
347   NSString *classString;
348   Class klazz;
349
350   if ([mimeType isEqualToString: @"image/gif"]
351       || [mimeType isEqualToString: @"image/png"]
352       || [mimeType isEqualToString: @"image/jpg"]
353       || [mimeType isEqualToString: @"image/jpeg"])
354     classString = @"SOGoImageMailBodyPart";
355   else if ([mimeType isEqualToString: @"text/calendar"])
356     classString = @"SOGoCalendarMailBodyPart";
357   else if ([mimeType isEqualToString: @"text/x-vcard"])
358     classString = @"SOGoVCardMailBodyPart";
359   else if ([mimeType isEqualToString: @"message/rfc822"])
360     classString = @"SOGoMessageMailBodyPart";
361   else
362     {
363       NSLog (@"unhandled mime type: '%@'", mimeType);
364       classString = nil;
365     }
366
367   if (classString)
368     klazz = NSClassFromString (classString);
369   else
370     klazz = [SOGoMailBodyPart class];
371
372   return klazz;
373 }
374
375 /* etag support */
376
377 - (id)davEntityTag {
378   return mailETag;
379 }
380
381 /* debugging */
382
383 - (BOOL)isDebuggingEnabled {
384   return debugOn;
385 }
386
387 @end /* SOGoMailBodyPart */