/* copyright 2002 Alexander Malmberg */ #include #include #include #include #include #include #include #include #include #include #include #include "MessageViewController.h" #include "Pref_MessageViewing.h" #include "main.h" #include "GUISource.h" #include #include #include #include #include //#define USE_MIME_STUFF #ifdef USE_MIME_STUFF #include static MimeManager *mime_manager; #endif static int get_depth(NSString *t,int start,int end) { int i; int nd; unichar uc; for (i=start,nd=0;i') nd++; else if (uc>32) break; } return nd; } /* we intentionally do _not_ retain object */ @interface MessageViewController_Link : NSObject { #define LINK_SOURCE 1 #define LINK_MULTIPART_ALTERNATIVE 2 #define LINK_DATA_SHOW 3 #define LINK_DATA_SHOW_TEXT 4 #define LINK_DATA_SAVE 5 #define LINK_DOWNLOAD 6 @public int type; id object; int value; } + linkWithType: (int)t object: (id)o value: (int)v; - initWithType: (int)t object: (id)o value: (int)v; @end @implementation MessageViewController_Link + linkWithType: (int)t object: (id)o value: (int)v { return [[[self alloc] initWithType: t object: o value: v] autorelease]; } - initWithType: (int)t object: (id)o value: (int)v { self=[super init]; type=t; object=o; value=v; return self; } @end @interface MessageViewController (private) -(void) _renderContent:(id)p parent:(Part *)parent to:(NSMutableAttributedString *)str; -(void) _renderPart:(id)p to:(NSMutableAttributedString *)str; -(void) displayMessage; -(void) getData; @end @implementation MessageViewController (private) static NSDictionary *colorForDepth(int d) { #define MAX_DEPTH 6 static NSDictionary *dicts[MAX_DEPTH]; if (d) { d=((d-1)%(MAX_DEPTH-1))+1; } if (!dicts[d]) { NSColor *c; switch (d) { default: case 0: c=[NSColor blackColor]; break; case 1: c=[NSColor blueColor]; break; case 2: c=[NSColor brownColor]; break; case 3: c=[NSColor purpleColor]; break; case 4: c=[NSColor orangeColor]; break; case 5: c=[NSColor redColor]; break; } dicts[d]=[[NSDictionary alloc] initWithObjectsAndKeys: c,NSForegroundColorAttributeName,nil]; } return dicts[d]; } static NSDictionary *header_bold_dict,*header_dict,*header_link_dict; /* helpers for _render* */ static void append(NSMutableAttributedString *str,NSDictionary *attrs, NSString *format,...) { va_list args; NSAttributedString *as; NSString *s; va_start(args,format); s=[[NSString alloc] initWithFormat:format arguments:args]; va_end(args); as=[[NSAttributedString alloc] initWithString:s attributes:attrs]; [str appendAttributedString: as]; DESTROY(as); DESTROY(s); } static void appendLink(NSMutableAttributedString *str,int type,id object, int value,NSString *format, ...) { va_list args; NSMutableAttributedString *as; NSString *s; va_start(args,format); s=[[NSString alloc] initWithFormat: format arguments: args]; va_end(args); as=[[NSMutableAttributedString alloc] initWithString:s attributes:header_link_dict]; [as addAttribute: NSLinkAttributeName value: [MessageViewController_Link linkWithType: type object: object value: value] range: NSMakeRange(0,[as length])]; [str appendAttributedString: as]; DESTROY(as); DESTROY(s); } static void appendPlainText(NSMutableAttributedString *str, NSDictionary *fd,NSString *text) { int len; len=[str length]; [str replaceCharactersInRange: NSMakeRange(len,0) withString: text]; [str setAttributes: fd range: NSMakeRange(len,[text length])]; if ([Pref_MessageViewing colorMessages]) { int i,j,d; for (i=j=0;i<[text length];i++) { if ([text characterAtIndex: i]=='\n') { if (i>j) { d=get_depth(text,j,i); /*if (d)*/ [str addAttributes: colorForDepth(d) range: NSMakeRange(len+j,i-j)]; } j=i+1; } } if (i>j) { d=get_depth(text,j,i); /*if (d)*/ [str addAttributes: colorForDepth(d) range: NSMakeRange(len+j,i-j)]; } } append(str,nil,@"\n"); } -(void) _render_multipart: (MimeMultipart *)mp type: (NSString *)ct to: (NSMutableAttributedString *)str { Part *bp; int i; if ([ct isEqual: @"multipart/alternative"]) { int disp; if (cur_options && (disp=(int)NSMapGet(cur_options,mp))) disp--; else disp=[mp count]-1; bp=[mp bodyPartAtIndex: disp]; append(str,header_bold_dict,_(@"Currently shown:")); append(str,header_dict,@" %@\n",[bp contentType]); append(str,header_bold_dict,_(@"Alternatives:")); for (i=0;i<[mp count];i++) { appendLink(str,LINK_MULTIPART_ALTERNATIVE,mp,i+1,@" %@", [[mp bodyPartAtIndex: i] contentType]); } append(str,nil,@"\n"); [self _renderPart: bp to:str]; } else { /* handle multipart/mixed and everything we don't understand */ int i; if (![ct isEqual: @"multipart/mixed"]) append(str,header_bold_dict,_(@"Displaying multipart with %i parts: %@\n"), [mp count],ct); for (i=0;i<[mp count];i++) { bp=[mp bodyPartAtIndex: i]; append(str,header_bold_dict,_(@"\nPart:")); append(str,header_dict,@" %i %@\n",i,[bp contentType]); [self _renderPart:bp to:str]; } } } -(void) _render_article_text: (NSString *)text to: (NSMutableAttributedString *)str { NSDictionary *fd; NSFont *f; NSRange r; if (cur_font) f=[Pref_MessageViewing font2]; else f=[Pref_MessageViewing font1]; fd=[[NSDictionary alloc] initWithObjectsAndKeys: f,NSFontAttributeName,nil]; #if 0 /* TODO: need to update with new Pantomime interface */ /* TODO: this should probably be optional */ r=[MimeUtility rangeOfUUEncodedStringFromString: text range: NSMakeRange(0,[text length])]; if (r.location==NSNotFound) #endif appendPlainText(str,fd,text); #if 0 else { NSFileWrapper *fw; if (r.location>0) appendPlainText(str,fd,[text substringWithRange: NSMakeRange(0,r.location)]); /* TODO: this is inefficient */ fw=[MimeUtility fileWrapperFromUUEncodedString: [text substringWithRange: r]]; append(str,header_bold_dict,_(@"UUEncoded file:")); append(str,header_dict,@" %@ ",[fw preferredFilename]); appendLink(str,LINK_DATA_SAVE,text,0,_(@"Save...")); append(str,header_dict,@"\n"); append(str,header_bold_dict,_(@"Size:")); append(str,header_dict,@" %i\n",[[fw regularFileContents] length]); DESTROY(fw); if (NSMaxRange(r)<[text length]) appendPlainText(str,fd,[text substringWithRange: NSMakeRange(NSMaxRange(r),[text length]-NSMaxRange(r))]); } #endif DESTROY(fd); } -(void) _renderContent:(id)p parent:(Part *)parent to:(NSMutableAttributedString *)str { NSString *ct=[parent contentType]; #ifdef USE_MIME_STUFF NSObject *mh; #endif NSData *d; NSString *s; int dbytes; /* Always display these if they are available. */ if ([parent contentDescription]) { append(str,header_bold_dict,_(@"Description:")); append(str,header_dict,@" %@\n",[parent contentDescription]); } if ([parent filename]) { append(str,header_bold_dict,_(@"Filename:")); append(str,header_dict,@" %@\n",[parent filename]); } /* Some content classes/types we handle internally. */ if ([p isKindOfClass: [MimeMultipart class]]) { [self _render_multipart: (MimeMultipart *)p type: ct to: str]; return; } if ([p isKindOfClass: [NSString class]] && [ct isEqual: @"text/plain"]) { [self _render_article_text: (NSString *)p to: str]; return; } if ([p isKindOfClass: [Part class]]) { [self _renderPart: p to: str]; return; } /* We don't know how to handle it, let all the mime stuff try. */ if ([p isKindOfClass: [NSString class]]) { s=(NSString *)p; d=nil; #ifdef USE_MIME_STUFF mh=[mime_manager mimeHandlerWithString: s type: ct]; #endif } else if ([p isKindOfClass: [NSData class]]) { s=nil; d=(NSData *)p; #ifdef USE_MIME_STUFF mh=[mime_manager mimeHandlerWithData: d type: ct]; #endif } else { append(str,header_bold_dict, _(@"Unable to render content of class '%@' (type '%@'). Please report this as a bug.\n"), NSStringFromClass([p class]),ct); return; } #ifdef USE_MIME_STUFF AUTORELEASE(mh); #endif /* Common header and options for all types. */ append(str,header_bold_dict,_(@"Type:")); append(str,header_dict,@" %@\n",ct); if (d) { append(str,header_bold_dict,_(@"Size:")); append(str,header_dict,@" %i\n ",[d length]); } appendLink(str,LINK_DATA_SAVE,parent,0,_(@"Save...")); dbytes=0; if (d) { append(str,nil,@" "); if (cur_options && ((int)NSMapGet(cur_options,p))==1) { appendLink(str,LINK_DATA_SHOW,p,0,_(@"Hide raw data")); dbytes=[d length]; if (dbytes>1024) dbytes=1024; } else { appendLink(str,LINK_DATA_SHOW,p,0,_(@"Show raw data")); } } append(str,nil,@" "); if (cur_options && ((int)NSMapGet(cur_options,p))==2) { appendLink(str,LINK_DATA_SHOW_TEXT,p,0,_(@"Hide text")); } else { appendLink(str,LINK_DATA_SHOW_TEXT,p,0,_(@"Try to show as text")); } append(str,nil,@"\n"); if (cur_options) { if (dbytes && d) { int i; NSData *d=(NSData *)p; const unsigned char *b; NSDictionary *fd; fd=[NSDictionary dictionaryWithObjectsAndKeys: [NSFont userFixedPitchFontOfSize: 0],NSFontAttributeName,nil]; b=[d bytes]; for (i=0;i)[s objectAtIndex: i] sourceName], [(id)[s objectAtIndex: i] sourceType]); } } } [ms endEditing]; } } @end @implementation MessageViewController -(BOOL) textView: (NSTextView *)t clickedOnLink: (MessageViewController_Link *)l atIndex: (unsigned int)idx { if (![l isKindOfClass: [MessageViewController_Link class]]) return NO; if (l->type==LINK_SOURCE) { int i,c; NSArray *s; /* make sure the source still exists */ s=[mdb sources]; c=[s count]; for (i=0;iobject) { [mdb msg_setSource: l->object : mid]; [mdb msg_needData: mid]; return YES; } } NSBeep(); return YES; } if (l->type==LINK_MULTIPART_ALTERNATIVE) { if (!cur_options) return YES; /* shouldn't happen */ NSMapInsert(cur_options,l->object,(void *)l->value); [self displayMessage]; return YES; } if (l->type==LINK_DATA_SHOW) { if (!cur_options) return YES; if (((int)NSMapGet(cur_options,l->object))==1) NSMapRemove(cur_options,l->object); else NSMapInsert(cur_options,l->object,(void *)1); [self displayMessage]; return YES; } if (l->type==LINK_DATA_SHOW_TEXT) { if (!cur_options) return YES; if (((int)NSMapGet(cur_options,l->object))==2) NSMapRemove(cur_options,l->object); else NSMapInsert(cur_options,l->object,(void *)2); [self displayMessage]; return YES; } if (l->type==LINK_DATA_SAVE) { NSSavePanel *sp; int result; NSData *d; NSString *filename; if ([l->object isKindOf: [NSString class]]) { NSString *s=(NSString *)l->object; NSRange r; UUFile *fw; r=[MimeUtility rangeOfUUEncodedStringFromString: s range: NSMakeRange(0,[s length])]; fw=[MimeUtility fileFromUUEncodedString: [s substringWithRange: r]]; filename=AUTORELEASE(RETAIN([fw name])); d=AUTORELEASE(RETAIN([fw data])); } else { Part *p=(Part *)l->object; d=(NSData *)[p content]; filename=[p filename]; } sp=[NSSavePanel savePanel]; [sp setTitle: _(@"Save data")]; if (filename) { result=[sp runModalForDirectory: [[NSFileManager defaultManager] currentDirectoryPath] file: filename]; } else result=[sp runModal]; if (result!=NSOKButton) return YES; { FILE *f=fopen([[[sp filename] stringByStandardizingPath] fileSystemRepresentation],"wb"); if (!f) { NSBeep(); /* TODO */ return YES; } if (fwrite([d bytes],[d length],1,f)!=1) NSBeep(); fclose(f); } return YES; } if (l->type==LINK_DOWNLOAD) { [mdb msg_needData: mid]; return YES; } NSLog(@"unknown link type %i\n",l->type); return YES; } -(void) messageUpdate: (MidNotification *)n { if ([n mid]!=mid) return; [self getData]; } - initWithMsgDB: (MsgDB *)m textView: (NSTextView *)textview scrollView: (NSScrollView *)scrollview; { if (!(self=[super init])) return nil; if (!header_bold_dict) { /* TODO */ NSColor *fore_color=[NSColor colorWithCalibratedRed:0.1 green:0.1 blue:0.6 alpha: 1.0]; // NSColor *back_color=[NSColor colorWithCalibratedRed:0.7 green:0.7 blue:0.7 alpha: 1.0]; header_bold_dict=[[NSDictionary dictionaryWithObjectsAndKeys: // back_color,NSBackgroundColorAttributeName, fore_color,NSForegroundColorAttributeName, [NSFont boldSystemFontOfSize: 0],NSFontAttributeName, nil] retain]; header_dict=[[NSDictionary dictionaryWithObjectsAndKeys: // back_color,NSBackgroundColorAttributeName, fore_color,NSForegroundColorAttributeName, [NSFont userFontOfSize: 0],NSFontAttributeName, nil] retain]; header_link_dict=[[NSDictionary dictionaryWithObjectsAndKeys: // back_color,NSBackgroundColorAttributeName, [NSColor redColor],NSForegroundColorAttributeName, [NSFont userFontOfSize: 0],NSFontAttributeName, nil] retain]; } #ifdef USE_MIME_STUFF if (!mime_manager) { mime_manager=[[MimeManager alloc] init]; } #endif ASSIGN(mdb,m); ASSIGN(tv,textview); ASSIGN(sv,scrollview); [tv setDelegate: self]; [[NSNotificationCenter defaultCenter] addObserver: self selector: @selector(messageUpdate:) name: MsgDB_MsgDStatusNotification object: mdb]; mid=0; return self; } -(void) dealloc { if (cur_options) { NSFreeMapTable(cur_options); cur_options=NULL; } DESTROY(cur_message); [[NSNotificationCenter defaultCenter] removeObserver: self]; [tv setDelegate: nil]; DESTROY(tv); DESTROY(sv); DESTROY(mdb); [super dealloc]; } -(void) setMid: (msg_id_t)m { if (mid==m) return; if (mid) [mdb msg_cancelWantData: mid]; mid=m; [self getData]; } -(BOOL) atEnd { NSRect r1=[[sv contentView] documentRect]; NSRect r2=[[sv contentView] documentVisibleRect]; NSRect r3=[[sv contentView] frame]; if (r2.origin.y+r3.size.height16) y-=16; idx=[[tv layoutManager] glyphIndexForPoint: NSMakePoint(0,y) inTextContainer: [tv textContainer]]; i2=idx; /* First skip backwards and check if the current line is quoted */ for (;idx>=0;idx--) if ([s characterAtIndex: idx]=='\n') break; idx++; /* Find the first unquoted line */ for (;idx<[s length];) { for (;idx<[s length];idx++) { ch=[s characterAtIndex: idx]; if (ch=='>' || ch>32) break; } if (idx>=[s length]) break; if ([s characterAtIndex: idx]=='>') { for (idx++;idx<[s length];idx++) if ([s characterAtIndex: idx]=='\n') break; idx++; } else break; } /* nothing more interesting in this message? */ if (idx>=[s length]) return YES; /* if we're already past the signature, return YES */ if (sig_index!=NSNotFound && idx>sig_index) return YES; /* if we're on a really long, unquoted line we want to continue from the point in the line we were at before */ if (idx