59f8615989
Monotone-Revision: f3c4d5f9e17b1f8f35c5fd70ef9c801ba53ee0dd Monotone-Author: wsourdeau@inverse.ca Monotone-Date: 2006-11-09T22:18:25 Monotone-Branch: ca.inverse.sogo
633 lines
14 KiB
Objective-C
633 lines
14 KiB
Objective-C
/*
|
|
Copyright (C) 2004-2005 SKYRIX Software AG
|
|
|
|
This file is part of OpenGroupware.org.
|
|
|
|
OGo is free software; you can redistribute it and/or modify it under
|
|
the terms of the GNU Lesser General Public License as published by the
|
|
Free Software Foundation; either version 2, or (at your option) any
|
|
later version.
|
|
|
|
OGo is distributed in the hope that it will be useful, but WITHOUT ANY
|
|
WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
|
FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
|
|
License for more details.
|
|
|
|
You should have received a copy of the GNU Lesser General Public
|
|
License along with OGo; see the file COPYING. If not, write to the
|
|
Free Software Foundation, 59 Temple Place - Suite 330, Boston, MA
|
|
02111-1307, USA.
|
|
*/
|
|
|
|
/*
|
|
UIxMailListView
|
|
|
|
This component represent a list of mails and is attached to an SOGoMailFolder
|
|
object.
|
|
*/
|
|
|
|
#define messagesPerPage 50
|
|
|
|
#include "common.h"
|
|
#include <SoObjects/Mailer/SOGoMailFolder.h>
|
|
#include <SoObjects/Mailer/SOGoMailObject.h>
|
|
#include <NGObjWeb/SoObject+SoDAV.h>
|
|
|
|
#import "UIxMailListView.h"
|
|
|
|
static int attachmentFlagSize = 8096;
|
|
|
|
@implementation UIxMailListView
|
|
|
|
- (void) dealloc
|
|
{
|
|
[self->qualifier release];
|
|
[self->sortedUIDs release];
|
|
[self->messages release];
|
|
[self->message release];
|
|
[super dealloc];
|
|
}
|
|
|
|
/* notifications */
|
|
|
|
- (void) sleep
|
|
{
|
|
[self->qualifier release]; self->qualifier = nil;
|
|
[self->sortedUIDs release]; self->sortedUIDs = nil;
|
|
[self->messages release]; self->messages = nil;
|
|
[self->message release]; self->message = nil;
|
|
[super sleep];
|
|
}
|
|
|
|
/* accessors */
|
|
|
|
- (void)setMessage:(id)_msg
|
|
{
|
|
ASSIGN(self->message, _msg);
|
|
}
|
|
|
|
- (id) message
|
|
{
|
|
return self->message;
|
|
}
|
|
|
|
- (void) setQualifier: (EOQualifier *) _msg
|
|
{
|
|
ASSIGN(self->qualifier, _msg);
|
|
}
|
|
|
|
- (EOQualifier *) qualifier
|
|
{
|
|
return self->qualifier;
|
|
}
|
|
|
|
- (BOOL) showToAddress
|
|
{
|
|
NSString *ftype;
|
|
|
|
ftype = [[self clientObject] valueForKey:@"outlookFolderClass"];
|
|
return [ftype isEqual:@"IPF.Sent"];
|
|
}
|
|
|
|
/* title */
|
|
|
|
- (NSString *) objectTitle
|
|
{
|
|
return [[self clientObject] nameInContainer];
|
|
}
|
|
|
|
- (NSString *) panelTitle
|
|
{
|
|
NSString *s;
|
|
|
|
s = [self labelForKey:@"View Mail Folder"];
|
|
s = [s stringByAppendingString:@": "];
|
|
s = [s stringByAppendingString:[self objectTitle]];
|
|
return s;
|
|
}
|
|
|
|
/* derived accessors */
|
|
|
|
- (BOOL) isMessageDeleted
|
|
{
|
|
NSArray *flags;
|
|
|
|
flags = [[self message] valueForKey:@"flags"];
|
|
return [flags containsObject:@"deleted"];
|
|
}
|
|
|
|
- (BOOL) isMessageRead
|
|
{
|
|
NSArray *flags;
|
|
|
|
flags = [[self message] valueForKey:@"flags"];
|
|
return [flags containsObject:@"seen"];
|
|
}
|
|
- (NSString *) messageUidString
|
|
{
|
|
return [[[self message] valueForKey:@"uid"] stringValue];
|
|
}
|
|
|
|
- (NSString *) messageCellStyleClass
|
|
{
|
|
return [self isMessageDeleted]
|
|
? @"mailer_listcell_deleted"
|
|
: @"mailer_listcell_regular";
|
|
}
|
|
|
|
- (NSString *) messageSubjectCellStyleClass
|
|
{
|
|
return [NSString stringWithFormat: @"%@ %@",
|
|
[self messageCellStyleClass],
|
|
([self isMessageRead]
|
|
? @"mailer_readmailsubject"
|
|
: @"mailer_unreadmailsubject")];
|
|
}
|
|
|
|
- (BOOL) hasMessageAttachment
|
|
{
|
|
/* we detect attachments by size ... */
|
|
unsigned size;
|
|
|
|
size = [[[self message] valueForKey:@"size"] intValue];
|
|
return size > attachmentFlagSize;
|
|
}
|
|
|
|
/* fetching messages */
|
|
|
|
- (NSArray *) fetchKeys
|
|
{
|
|
/* Note: see SOGoMailManager.m for allowed IMAP4 keys */
|
|
static NSArray *keys = nil;
|
|
if (keys == nil) {
|
|
keys = [[NSArray alloc] initWithObjects:
|
|
@"FLAGS", @"ENVELOPE", @"RFC822.SIZE", nil];
|
|
}
|
|
return keys;
|
|
}
|
|
|
|
- (NSString *) defaultSortKey
|
|
{
|
|
return @"DATE";
|
|
}
|
|
|
|
- (NSString *) imap4SortKey
|
|
{
|
|
NSString *sort;
|
|
|
|
sort = [[[self context] request] formValueForKey:@"sort"];
|
|
|
|
if ([sort length] == 0)
|
|
sort = [self defaultSortKey];
|
|
|
|
return [sort uppercaseString];
|
|
}
|
|
|
|
- (BOOL) isSortedDescending
|
|
{
|
|
NSString *desc;
|
|
|
|
desc = [[[self context] request] formValueForKey:@"desc"];
|
|
|
|
return ((desc)
|
|
? [desc boolValue]
|
|
: YES);
|
|
}
|
|
|
|
- (NSString *) imap4SortOrdering
|
|
{
|
|
NSString *sort;
|
|
|
|
sort = [self imap4SortKey];
|
|
|
|
if ([self isSortedDescending])
|
|
sort = [@"REVERSE " stringByAppendingString: sort];
|
|
|
|
return sort;
|
|
}
|
|
|
|
- (NSRange) fetchRange
|
|
{
|
|
if (self->firstMessageNumber == 0)
|
|
return NSMakeRange(0, messagesPerPage);
|
|
return NSMakeRange(self->firstMessageNumber - 1, messagesPerPage);
|
|
}
|
|
|
|
- (NSArray *) sortedUIDs
|
|
{
|
|
if (!sortedUIDs)
|
|
{
|
|
sortedUIDs
|
|
= [[self clientObject] fetchUIDsMatchingQualifier: [self qualifier]
|
|
sortOrdering: [self imap4SortOrdering]];
|
|
[sortedUIDs retain];
|
|
}
|
|
|
|
return self->sortedUIDs;
|
|
}
|
|
|
|
- (unsigned int) totalMessageCount
|
|
{
|
|
return [self->sortedUIDs count];
|
|
}
|
|
|
|
- (BOOL) showsAllMessages
|
|
{
|
|
return ([[self sortedUIDs] count] <= [self fetchRange].length) ? YES : NO;
|
|
}
|
|
|
|
- (NSRange) fetchBlock
|
|
{
|
|
NSRange r;
|
|
unsigned len;
|
|
NSArray *uids;
|
|
|
|
r = [self fetchRange];
|
|
uids = [self sortedUIDs];
|
|
|
|
/* only need to restrict if we have a lot */
|
|
if ((len = [uids count]) <= r.length) {
|
|
r.location = 0;
|
|
r.length = len;
|
|
return r;
|
|
}
|
|
|
|
if (len < r.location) {
|
|
// TODO: CHECK CONDITION (< vs <=)
|
|
/* out of range, recover at first block */
|
|
r.location = 0;
|
|
return r;
|
|
}
|
|
|
|
if (r.location + r.length > len)
|
|
r.length = len - r.location;
|
|
return r;
|
|
}
|
|
|
|
- (unsigned int) firstMessageNumber
|
|
{
|
|
return [self fetchBlock].location + 1;
|
|
}
|
|
|
|
- (unsigned int) lastMessageNumber
|
|
{
|
|
NSRange r;
|
|
|
|
r = [self fetchBlock];
|
|
return r.location + r.length;
|
|
}
|
|
|
|
- (BOOL) hasPrevious
|
|
{
|
|
return [self fetchBlock].location == 0 ? NO : YES;
|
|
}
|
|
|
|
- (BOOL) hasNext
|
|
{
|
|
NSRange r = [self fetchBlock];
|
|
return r.location + r.length >= [[self sortedUIDs] count] ? NO : YES;
|
|
}
|
|
|
|
- (unsigned int) nextFirstMessageNumber
|
|
{
|
|
return [self firstMessageNumber] + [self fetchRange].length;
|
|
}
|
|
|
|
- (unsigned int) prevFirstMessageNumber
|
|
{
|
|
NSRange r;
|
|
unsigned idx;
|
|
|
|
idx = [self firstMessageNumber];
|
|
r = [self fetchRange];
|
|
if (idx > r.length)
|
|
return (idx - r.length);
|
|
return 1;
|
|
}
|
|
|
|
- (NSArray *) messages
|
|
{
|
|
NSArray *uids;
|
|
NSArray *msgs;
|
|
NSRange r;
|
|
unsigned len;
|
|
|
|
if (self->messages != nil)
|
|
return self->messages;
|
|
|
|
r = [self fetchBlock];
|
|
uids = [self sortedUIDs];
|
|
if ((len = [uids count]) > r.length)
|
|
/* only need to restrict if we have a lot */
|
|
uids = [uids subarrayWithRange:r];
|
|
|
|
msgs = [[self clientObject] fetchUIDs:uids parts:[self fetchKeys]];
|
|
self->messages = [[msgs valueForKey:@"fetch"] retain];
|
|
return self->messages;
|
|
}
|
|
|
|
/* URL processing */
|
|
|
|
- (NSString *) messageViewTarget
|
|
{
|
|
return [NSString stringWithFormat: @"SOGo_msg_%@",
|
|
[self messageUidString]];
|
|
}
|
|
|
|
- (NSString *) messageViewURL
|
|
{
|
|
// TODO: noframe only when view-target is empty
|
|
// TODO: markread only if the message is unread
|
|
NSString *s;
|
|
|
|
s = [[self messageUidString] stringByAppendingString:@"/view?noframe=1"];
|
|
if (![self isMessageRead]) s = [s stringByAppendingString:@"&markread=1"];
|
|
return s;
|
|
}
|
|
- (NSString *) markReadURL
|
|
{
|
|
return [@"markMessageRead?uid=" stringByAppendingString:
|
|
[self messageUidString]];
|
|
}
|
|
- (NSString *) markUnreadURL
|
|
{
|
|
return [@"markMessageUnread?uid=" stringByAppendingString:
|
|
[self messageUidString]];
|
|
}
|
|
|
|
/* JavaScript */
|
|
|
|
- (NSString *)msgRowID
|
|
{
|
|
return [@"row_" stringByAppendingString:[self messageUidString]];
|
|
}
|
|
|
|
- (NSString *)msgDivID
|
|
{
|
|
return [@"div_" stringByAppendingString:[self messageUidString]];
|
|
}
|
|
|
|
- (NSString *)msgIconReadImgID
|
|
{
|
|
return [@"readdiv_" stringByAppendingString:[self messageUidString]];
|
|
}
|
|
|
|
- (NSString *)msgIconUnreadImgID
|
|
{
|
|
return [@"unreaddiv_" stringByAppendingString:[self messageUidString]];
|
|
}
|
|
|
|
/* error redirects */
|
|
|
|
- (id) redirectToViewWithError: (id) _error
|
|
{
|
|
// TODO: DUP in UIxMailAccountView
|
|
// TODO: improve, localize
|
|
// TODO: there is a bug in the treeview which preserves the current URL for
|
|
// the active object (displaying the error again)
|
|
id url;
|
|
|
|
if (![_error isNotNull])
|
|
return [self redirectToLocation:@"view"];
|
|
|
|
if ([_error isKindOfClass:[NSException class]])
|
|
_error = [_error reason];
|
|
else if ([_error isKindOfClass:[NSString class]])
|
|
_error = [_error stringValue];
|
|
|
|
url = [_error stringByEscapingURL];
|
|
url = [@"view?error=" stringByAppendingString:url];
|
|
return [self redirectToLocation:url];
|
|
}
|
|
|
|
/* active message */
|
|
|
|
- (SOGoMailObject *) lookupActiveMessage
|
|
{
|
|
NSString *uid;
|
|
|
|
if ((uid = [[[self context] request] formValueForKey:@"uid"]) == nil)
|
|
return nil;
|
|
|
|
return [[self clientObject] lookupName:uid inContext:[self context]
|
|
acquire:NO];
|
|
}
|
|
|
|
/* actions */
|
|
|
|
- (BOOL) isJavaScriptRequest
|
|
{
|
|
return [[[[self context] request] formValueForKey:@"jsonly"] boolValue];
|
|
}
|
|
|
|
- (id) javaScriptOK
|
|
{
|
|
WOResponse *r;
|
|
|
|
r = [[self context] response];
|
|
[r setStatus:200 /* OK */];
|
|
return r;
|
|
}
|
|
|
|
- (int) firstMessageOfPageFor: (int) messageNbr
|
|
{
|
|
NSArray *messageNbrs;
|
|
int nbrInArray;
|
|
int firstMessage;
|
|
|
|
messageNbrs = [self sortedUIDs];
|
|
nbrInArray
|
|
= [messageNbrs indexOfObject: [NSNumber numberWithInt: messageNbr]];
|
|
if (nbrInArray > -1)
|
|
firstMessage = ((int) (nbrInArray / messagesPerPage)
|
|
* messagesPerPage) + 1;
|
|
else
|
|
firstMessage = 1;
|
|
|
|
return firstMessage;
|
|
}
|
|
|
|
- (id) defaultAction
|
|
{
|
|
WORequest *request;
|
|
NSString *specificMessage;
|
|
|
|
request = [[self context] request];
|
|
specificMessage = [request formValueForKey: @"pageforuid"];
|
|
self->firstMessageNumber
|
|
= ((specificMessage)
|
|
? [self firstMessageOfPageFor: [specificMessage intValue]]
|
|
: [[request formValueForKey:@"idx"] intValue]);
|
|
|
|
return self;
|
|
}
|
|
|
|
- (id) viewAction
|
|
{
|
|
return [self defaultAction];
|
|
}
|
|
|
|
- (id) markMessageUnreadAction
|
|
{
|
|
NSException *error;
|
|
|
|
if ((error = [[self lookupActiveMessage] removeFlags:@"seen"]) != nil)
|
|
// TODO: improve error handling
|
|
return error;
|
|
|
|
if ([self isJavaScriptRequest])
|
|
return [self javaScriptOK];
|
|
|
|
return [self redirectToLocation:@"view"];
|
|
}
|
|
|
|
- (id) markMessageReadAction
|
|
{
|
|
NSException *error;
|
|
|
|
if ((error = [[self lookupActiveMessage] addFlags:@"seen"]) != nil)
|
|
// TODO: improve error handling
|
|
return error;
|
|
|
|
if ([self isJavaScriptRequest])
|
|
return [self javaScriptOK];
|
|
|
|
return [self redirectToLocation:@"view"];
|
|
}
|
|
|
|
- (id) getMailAction
|
|
{
|
|
// TODO: we might want to flush the caches?
|
|
id client;
|
|
|
|
if ((client = [self clientObject]) == nil) {
|
|
return [NSException exceptionWithHTTPStatus:404 /* Not Found */
|
|
reason:@"did not find mail folder"];
|
|
}
|
|
|
|
if (![client respondsToSelector:@selector(flushMailCaches) ])
|
|
{
|
|
return [NSException exceptionWithHTTPStatus: 500 /* Server Error */
|
|
reason:
|
|
@"invalid client object (does not support flush)"];
|
|
}
|
|
|
|
[client flushMailCaches];
|
|
|
|
return [self redirectToLocation:@"view"];
|
|
}
|
|
|
|
- (id) expungeAction
|
|
{
|
|
// TODO: we might want to flush the caches?
|
|
NSException *error;
|
|
id client;
|
|
|
|
if ((client = [self clientObject]) == nil) {
|
|
return [NSException exceptionWithHTTPStatus:404 /* Not Found */
|
|
reason:@"did not find mail folder"];
|
|
}
|
|
|
|
if ((error = [[self clientObject] expunge]) != nil)
|
|
return error;
|
|
|
|
if ([client respondsToSelector:@selector(flushMailCaches)])
|
|
[client flushMailCaches];
|
|
return [self redirectToLocation:@"view"];
|
|
}
|
|
|
|
- (id) emptyTrashAction
|
|
{
|
|
// TODO: we might want to flush the caches?
|
|
NSException *error;
|
|
id client;
|
|
|
|
if ((client = [self clientObject]) == nil) {
|
|
error = [NSException exceptionWithHTTPStatus:404 /* Not Found */
|
|
reason:@"did not find mail folder"];
|
|
return [self redirectToViewWithError:error];
|
|
}
|
|
|
|
if (![client isKindOfClass:NSClassFromString(@"SOGoTrashFolder")]) {
|
|
/* would be better to move the method to an own class, but well .. */
|
|
error = [NSException exceptionWithHTTPStatus:400 /* Bad Request */
|
|
reason:@"method cannot be invoked on "
|
|
@"the specified object"];
|
|
return [self redirectToViewWithError:error];
|
|
}
|
|
|
|
/* mark all as deleted */
|
|
|
|
[self logWithFormat:@"TODO: must mark all as deleted for empty-trash"];
|
|
|
|
error = [[self clientObject] addFlagsToAllMessages:@"deleted"];
|
|
if (error != nil)
|
|
// TODO: improve error
|
|
return [self redirectToViewWithError:error];
|
|
|
|
/* expunge */
|
|
|
|
if ((error = [[self clientObject] expunge]) != nil)
|
|
// TODO: improve error
|
|
return [self redirectToViewWithError:error];
|
|
|
|
if ([client respondsToSelector:@selector(flushMailCaches)])
|
|
[client flushMailCaches];
|
|
return [self redirectToLocation:@"view"];
|
|
}
|
|
|
|
/* folder operations */
|
|
|
|
- (id) createFolderAction
|
|
{
|
|
NSException *error;
|
|
NSString *folderName;
|
|
id client;
|
|
|
|
folderName = [[[self context] request] formValueForKey:@"name"];
|
|
if ([folderName length] == 0) {
|
|
error = [NSException exceptionWithHTTPStatus:400 /* Bad Request */
|
|
reason:@"missing 'name' query parameter!"];
|
|
return [self redirectToViewWithError:error];
|
|
}
|
|
|
|
if ((client = [self clientObject]) == nil) {
|
|
error = [NSException exceptionWithHTTPStatus:404 /* Not Found */
|
|
reason:@"did not find mail folder"];
|
|
return [self redirectToViewWithError:error];
|
|
}
|
|
|
|
if ((error = [[self clientObject] davCreateCollection:folderName
|
|
inContext:[self context]]) != nil) {
|
|
return [self redirectToViewWithError:error];
|
|
}
|
|
|
|
return [self redirectToLocation:[folderName stringByAppendingString:@"/"]];
|
|
}
|
|
|
|
- (id) deleteFolderAction
|
|
{
|
|
NSException *error;
|
|
NSString *url;
|
|
id client;
|
|
|
|
if ((client = [self clientObject]) == nil) {
|
|
error = [NSException exceptionWithHTTPStatus:404 /* Not Found */
|
|
reason:@"did not find mail folder"];
|
|
return [self redirectToViewWithError:error];
|
|
}
|
|
|
|
/* jump to parent folder afterwards */
|
|
url = [[client container] baseURLInContext:[self context]];
|
|
if (![url hasSuffix:@"/"]) url = [url stringByAppendingString:@"/"];
|
|
|
|
if ((error = [[self clientObject] delete]) != nil)
|
|
return [self redirectToViewWithError:error];
|
|
|
|
return [self redirectToLocation:url];
|
|
}
|
|
|
|
@end
|
|
|
|
/* UIxMailListView */
|