From fbb998bc7b632cafb7111c6186f7f8e500f77692 Mon Sep 17 00:00:00 2001 From: helge Date: Mon, 11 Jul 2005 12:24:23 +0000 Subject: [PATCH] added NGImap4Connection API git-svn-id: http://svn.opengroupware.org/SOPE/trunk@881 e4a50df8-12e2-0310-a44c-efbce7f8a7e3 --- sope-mime/ChangeLog | 4 + sope-mime/NGImap4/ChangeLog | 5 + sope-mime/NGImap4/GNUmakefile | 35 +- sope-mime/NGImap4/NGImap4Connection.h | 136 +++ sope-mime/NGImap4/NGImap4Connection.m | 875 +++++++++++++++++++ sope-mime/NGImap4/NGImap4ConnectionManager.h | 54 ++ sope-mime/NGImap4/NGImap4ConnectionManager.m | 209 +++++ sope-mime/NGImap4/NGImap4MailboxInfo.h | 60 ++ sope-mime/NGImap4/NGImap4MailboxInfo.m | 101 +++ sope-mime/Version | 2 +- 10 files changed, 1466 insertions(+), 15 deletions(-) create mode 100644 sope-mime/NGImap4/NGImap4Connection.h create mode 100644 sope-mime/NGImap4/NGImap4Connection.m create mode 100644 sope-mime/NGImap4/NGImap4ConnectionManager.h create mode 100644 sope-mime/NGImap4/NGImap4ConnectionManager.m create mode 100644 sope-mime/NGImap4/NGImap4MailboxInfo.h create mode 100644 sope-mime/NGImap4/NGImap4MailboxInfo.m diff --git a/sope-mime/ChangeLog b/sope-mime/ChangeLog index 2521dd04..b1e9a590 100644 --- a/sope-mime/ChangeLog +++ b/sope-mime/ChangeLog @@ -1,3 +1,7 @@ +2005-07-11 Helge Hess + + * NGImap4: added NGImap4Connection, NGImap4ConnectionManager (v4.5.223) + 2005-07-07 Helge Hess * NGImap4: added method to store flags for MSN sequences (v4.5.222) diff --git a/sope-mime/NGImap4/ChangeLog b/sope-mime/NGImap4/ChangeLog index e2bbfe86..aec76084 100644 --- a/sope-mime/NGImap4/ChangeLog +++ b/sope-mime/NGImap4/ChangeLog @@ -1,3 +1,8 @@ +2005-07-11 Helge Hess + + * added NGImap4Connection/NGImap4ConnectionManager classes, a simpler + interface to the IMAP4 client library + 2005-07-07 Helge Hess * NGImap4Client.m: added -storeFlags:forMSNs:addOrRemove: method to diff --git a/sope-mime/NGImap4/GNUmakefile b/sope-mime/NGImap4/GNUmakefile index f2d59089..f534d97a 100644 --- a/sope-mime/NGImap4/GNUmakefile +++ b/sope-mime/NGImap4/GNUmakefile @@ -9,20 +9,23 @@ NGImap4_HEADER_FILES_DIR = . NGImap4_HEADER_FILES_INSTALL_DIR = /NGImap4 NGImap4_HEADER_FILES = \ - NGImap4ResponseParser.h \ - NGImap4Client.h \ - NGImap4Support.h \ - NGImap4Folder.h \ - NGImap4Context.h \ - NGImap4Message.h \ - NGImap4ServerRoot.h \ - NGImap4FileManager.h \ - NGImap4.h \ - NGImap4DataSource.h \ - NSString+Imap4.h \ - NGSieveClient.h \ - NGImap4Envelope.h \ - NGImap4EnvelopeAddress.h\ + NGImap4ResponseParser.h \ + NGImap4Client.h \ + NGImap4Support.h \ + NGImap4Folder.h \ + NGImap4Context.h \ + NGImap4Message.h \ + NGImap4ServerRoot.h \ + NGImap4FileManager.h \ + NGImap4.h \ + NGImap4DataSource.h \ + NSString+Imap4.h \ + NGSieveClient.h \ + NGImap4Envelope.h \ + NGImap4EnvelopeAddress.h \ + NGImap4Connection.h \ + NGImap4MailboxInfo.h \ + NGImap4ConnectionManager.h \ NGImap4_OBJC_FILES = \ NGImap4ResponseParser.m \ @@ -49,6 +52,10 @@ NGImap4_OBJC_FILES = \ NGImap4ResponseNormalizer.m \ NGImap4Envelope.m \ NGImap4EnvelopeAddress.m \ + \ + NGImap4Connection.m \ + NGImap4MailboxInfo.m \ + NGImap4ConnectionManager.m \ -include GNUmakefile.preamble include $(GNUSTEP_MAKEFILES)/subproject.make diff --git a/sope-mime/NGImap4/NGImap4Connection.h b/sope-mime/NGImap4/NGImap4Connection.h new file mode 100644 index 00000000..4d86fc95 --- /dev/null +++ b/sope-mime/NGImap4/NGImap4Connection.h @@ -0,0 +1,136 @@ +/* + 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. +*/ + +#ifndef __NGImap4Connection_H__ +#define __NGImap4Connection_H__ + +#import + +/* + NGImap4Connection + + A cached connection to an IMAP4 server plus some cached objects. Do not + instantiate this object directly but rather use the NGImap4ConnectionManager. + + This API is intended to be simpler and more consistent than NGImap4Client. + + It caches: + - the folder hierarchy + - uid sets? + - 'myrights' permissions of mailboxes + ? +*/ + +@class NSString, NSDate, NSArray, NSDictionary, NSURL, NSMutableDictionary; +@class NSException, NSData; +@class NGImap4Client; + +@interface NGImap4Connection : NSObject +{ +@public + NGImap4Client *client; + NSString *password; + NSDate *creationTime; + + /* hierarchy cache */ + NSDictionary *subfolders; + + /* permission cache */ + NSMutableDictionary *urlToRights; + + /* uids cache */ + NSArray *cachedUIDs; + NSURL *uidFolderURL; + id uidSortOrdering; +} + +- (id)initWithClient:(NGImap4Client *)_client password:(NSString *)_pwd; + +/* accessors */ + +- (NGImap4Client *)client; +- (BOOL)isValidPassword:(NSString *)_pwd; + +- (NSDate *)creationTime; + +- (void)cacheHierarchyResults:(NSDictionary *)_hierarchy; +- (NSDictionary *)cachedHierarchyResults; +- (void)flushFolderHierarchyCache; + +- (id)cachedUIDsForURL:(NSURL *)_url qualifier:(id)_q sortOrdering:(id)_so; +- (void)cacheUIDs:(NSArray *)_uids forURL:(NSURL *)_url + qualifier:(id)_q sortOrdering:(id)_so; + +- (NSString *)cachedMyRightsForURL:(NSURL *)_url; +- (void)cacheMyRights:(NSString *)_rights forURL:(NSURL *)_url; + +- (void)flushMailCaches; + +/* folder operations */ + +- (NSArray *)subfoldersForURL:(NSURL *)_url; +- (NSArray *)allFoldersForURL:(NSURL *)_url; + +/* message operations */ + +- (NSArray *)fetchUIDsInURL:(NSURL *)_url qualifier:(id)_qualifier + sortOrdering:(id)_so; +- (NSArray *)fetchUIDs:(NSArray *)_uids inURL:(NSURL *)_url + parts:(NSArray *)_parts; +- (id)fetchURL:(NSURL *)_url parts:(NSArray *)_parts; +- (NSData *)fetchContentOfBodyPart:(NSString *)_partId atURL:(NSURL *)_url; + +/* message flags */ + +- (NSException *)addOrRemove:(BOOL)_flag flags:(id)_f toURL:(NSURL *)_url; +- (NSException *)addFlags:(id)_f toURL:(NSURL *)_u; +- (NSException *)removeFlags:(id)_f toURL:(NSURL *)_u; +- (NSException *)markURLDeleted:(NSURL *)_url; +- (NSException *)addFlags:(id)_f toAllMessagesInURL:(NSURL *)_url; + +/* posting new data */ + +- (NSException *)postData:(NSData *)_data flags:(id)_f toFolderURL:(NSURL *)_u; + +/* operations */ + +- (NSException *)expungeAtURL:(NSURL *)_url; + +/* copying and moving */ + +- (NSException *)copyMailURL:(NSURL *)_srcurl toFolderURL:(NSURL *)_desturl; + +/* managing folders */ + +- (BOOL)doesMailboxExistAtURL:(NSURL *)_url; +- (id)infoForMailboxAtURL:(NSURL *)_url; +- (NSException *)createMailbox:(NSString *)_mailbox atURL:(NSURL *)_url; +- (NSException *)deleteMailboxAtURL:(NSURL *)_url; +- (NSException *)moveMailboxAtURL:(NSURL *)_srcurl toURL:(NSURL *)_desturl; + +/* ACLs */ + +- (NSDictionary *)aclForMailboxAtURL:(NSURL *)_url; +- (NSString *)myRightsForMailboxAtURL:(NSURL *)_url; + +@end + +#endif /* __NGImap4Connection_H__ */ diff --git a/sope-mime/NGImap4/NGImap4Connection.m b/sope-mime/NGImap4/NGImap4Connection.m new file mode 100644 index 00000000..7a1d779d --- /dev/null +++ b/sope-mime/NGImap4/NGImap4Connection.m @@ -0,0 +1,875 @@ +/* + 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. +*/ + +#include "NGImap4Connection.h" +#include "NGImap4MailboxInfo.h" +#include "NGImap4Client.h" +#include "imCommon.h" + +@implementation NGImap4Connection + +static BOOL debugOn = NO; +static BOOL debugCache = NO; +static BOOL debugKeys = NO; +static BOOL alwaysSelect = NO; +static BOOL onlyFetchInbox = NO; +static NSString *imap4Separator = nil; + ++ (void)initialize { + NSUserDefaults *ud = [NSUserDefaults standardUserDefaults]; + + debugOn = [ud boolForKey:@"SOGoEnableIMAP4Debug"]; + debugCache = [ud boolForKey:@"SOGoEnableIMAP4CacheDebug"]; + alwaysSelect = [ud boolForKey:@"SOGoAlwaysSelectIMAP4Folder"]; + if (debugOn) NSLog(@"Note: SOGoEnableIMAP4Debug is enabled!"); + if (alwaysSelect) + NSLog(@"WARNING: 'SOGoAlwaysSelectIMAP4Folder' enabled (slow down)"); + + imap4Separator = [[ud stringForKey:@"SOGoIMAP4StringSeparator"] copy]; + if ([imap4Separator length] == 0) + imap4Separator = @"/"; + NSLog(@"Note(SOGoMailManager): using '%@' as the IMAP4 folder separator.", + imap4Separator); +} + +- (id)initWithClient:(NGImap4Client *)_client password:(NSString *)_pwd { + if (_client == nil || _pwd == nil) { + [self release]; + return nil; + } + + if ((self = [super init])) { + self->client = [_client retain]; + self->password = [_pwd copy]; + + self->creationTime = [[NSDate alloc] init]; + } + return self; +} +- (id)init { + return [self initWithClient:nil password:nil]; +} + +- (void)dealloc { + [self->urlToRights release]; + [self->cachedUIDs release]; + [self->uidFolderURL release]; + [self->uidSortOrdering release]; + [self->creationTime release]; + [self->subfolders release]; + [self->password release]; + [self->client release]; + [super dealloc]; +} + +/* accessors */ + +- (NGImap4Client *)client { + return self->client; +} +- (BOOL)isValidPassword:(NSString *)_pwd { + return [self->password isEqualToString:_pwd]; +} + +- (NSDate *)creationTime { + return self->creationTime; +} + +- (void)cacheHierarchyResults:(NSDictionary *)_hierarchy { + ASSIGNCOPY(self->subfolders, _hierarchy); +} +- (NSDictionary *)cachedHierarchyResults { + return self->subfolders; +} +- (void)flushFolderHierarchyCache { + [self->subfolders release]; self->subfolders = nil; + [self->urlToRights release]; self->urlToRights = nil; +} + +/* rights */ + +- (NSString *)cachedMyRightsForURL:(NSURL *)_url { + return (_url != nil) ? [self->urlToRights objectForKey:_url] : nil; +} +- (void)cacheMyRights:(NSString *)_rights forURL:(NSURL *)_url { + if (self->urlToRights == nil) + self->urlToRights = [[NSMutableDictionary alloc] initWithCapacity:8]; + [self->urlToRights setObject:_rights forKey:_url]; +} + +/* UIDs */ + +- (id)cachedUIDsForURL:(NSURL *)_url qualifier:(id)_q sortOrdering:(id)_so { + if (_q != nil) + return nil; + if (![_so isEqual:self->uidSortOrdering]) + return nil; + if (![self->uidFolderURL isEqual:_url]) + return nil; + + return self->cachedUIDs; +} + +- (void)cacheUIDs:(NSArray *)_uids forURL:(NSURL *)_url + qualifier:(id)_q sortOrdering:(id)_so +{ + if (_q != nil) + return; + + ASSIGNCOPY(self->uidSortOrdering, _so); + ASSIGNCOPY(self->uidFolderURL, _url); + ASSIGNCOPY(self->cachedUIDs, _uids); +} + +- (void)flushMailCaches { + ASSIGN(self->uidSortOrdering, nil); + ASSIGN(self->uidFolderURL, nil); + ASSIGN(self->cachedUIDs, nil); +} + + +/* errors */ + +- (NSException *)errorCouldNotSelectURL:(NSURL *)_url { + NSDictionary *ui; + NSString *r; + + r = [_url isNotNull] + ? [@"Could not select IMAP4 folder: " stringByAppendingString: + [_url absoluteString]] + : @"Could not select IMAP4 folder!"; + + ui = [NSDictionary dictionaryWithObjectsAndKeys: + [NSNumber numberWithInt:404], @"http-status", + _url, @"url", + nil]; + + return [NSException exceptionWithName:@"NGImap4Exception" + reason:r userInfo:ui]; +} + +- (NSException *)errorForResult:(NSDictionary *)_result text:(NSString *)_txt { + NSDictionary *ui; + NSString *r; + int status; + + if ([[_result valueForKey:@"result"] boolValue]) + return nil; /* everything went fine! */ + + if ((r = [_result valueForKey:@"reason"]) != nil) + r = [[_txt stringByAppendingString:@": "] stringByAppendingString:r]; + else + r = _txt; + + if ([r isEqualToString:@"Permission denied"]) { + /* different for each server?, no error codes in IMAP4 ... */ + status = 403 /* Forbidden */; + } + else + status = 500 /* internal server error */; + + ui = [NSDictionary dictionaryWithObjectsAndKeys: + [NSNumber numberWithInt:status], @"http-status", + _result, @"rawResult", + nil]; + + return [NSException exceptionWithName:@"NGImap4Exception" + reason:r userInfo:ui]; +} + +/* IMAP4 path/url processing methods */ + +NSArray *SOGoMailGetDirectChildren(NSArray *_array, NSString *_fn) { + /* + Scans string '_array' for strings which start with the string in '_fn'. + Then split on '/'. + */ + NSMutableArray *ma; + unsigned i, count, prefixlen; + + if ((count = [_array count]) < 2) + /* one entry is the folder itself, so we need at least two */ + return [NSArray array]; + +#if __APPLE__ + // TODO: somehow results are different on OSX + prefixlen = [_fn isEqualToString:@""] ? 0 : [_fn length] + 1; +#else + prefixlen = [_fn isEqualToString:@"/"] ? 1 : [_fn length] + 1; +#endif + ma = [NSMutableArray arrayWithCapacity:count]; + for (i = 0; i < count; i++) { + NSString *p; + + p = [_array objectAtIndex:i]; + if ([p length] <= prefixlen) + continue; + if (prefixlen != 0 && ![p hasPrefix:_fn]) + continue; + + /* cut of common part */ + p = [p substringFromIndex:prefixlen]; + + /* check whether the path is a sub-subfolder path */ + if ([p rangeOfString:@"/"].length > 0) + continue; + + [ma addObject:p]; + } + + [ma sortUsingSelector:@selector(compare:)]; + return ma; +} + +NSArray *SOGoMailExtractSubfolders(NSURL *_url, NSDictionary *_result) { + NSString *folderName; + NSDictionary *result; + NSArray *names; + NSArray *flags; + + /* Note: the result is normalized, that is, it contains / as the separator */ + folderName = [_url path]; +#if __APPLE__ + /* normalized results already have the / in front on libFoundation?! */ + if ([folderName hasPrefix:@"/"]) + folderName = [folderName substringFromIndex:1]; +#endif + + result = [_result valueForKey:@"list"]; + + /* Cyrus already tells us whether we need to check for children */ + flags = [result objectForKey:folderName]; + if ([flags containsObject:@"hasnochildren"]) { + if (debugKeys) + NSLog(@"%s: folder %@ has no children.", __PRETTY_FUNCTION__,folderName); + return nil; + } + + if (debugKeys) { + NSLog(@"%s: all keys %@: %@", __PRETTY_FUNCTION__, folderName, + [[result allKeys] componentsJoinedByString:@", "]); + } + + names = SOGoMailGetDirectChildren([result allKeys], folderName); + if (debugKeys) { + NSLog(@"%s: subfolders of '%@': %@", __PRETTY_FUNCTION__, folderName, + [names componentsJoinedByString:@","]); + } + return names; +} + +- (NSArray *)_getDirectChildren:(NSArray *)_array folderName:(NSString *)_fn { + return SOGoMailGetDirectChildren(_array, _fn); +} +- (NSArray *)extractSubfoldersForURL:(NSURL *)_url + fromResultSet:(NSDictionary *)_result +{ + return SOGoMailExtractSubfolders(_url, _result); +} + +- (NSString *)imap4Separator { + // TODO: make server specific ivar! + return imap4Separator; +} + +- (NSString *)imap4FolderNameForURL:(NSURL *)_url removeFileName:(BOOL)_delfn { + /* a bit hackish, but should be OK */ + NSString *folderName; + NSArray *names; + + if (_url == nil) + return nil; + + folderName = [_url path]; + if ([folderName length] == 0) + return nil; + if ([folderName characterAtIndex:0] == '/') + folderName = [folderName substringFromIndex:1]; + + if (_delfn) folderName = [folderName stringByDeletingLastPathComponent]; + + if ([[self imap4Separator] isEqualToString:@"/"]) + return folderName; + + names = [folderName pathComponents]; + return [names componentsJoinedByString:[self imap4Separator]]; +} +- (NSString *)imap4FolderNameForURL:(NSURL *)_url { + return [self imap4FolderNameForURL:_url removeFileName:NO]; +} + +- (NSArray *)extractFoldersFromResultSet:(NSDictionary *)_result { + /* Note: the result is normalized, that is, it contains / as the separator */ + return [[_result valueForKey:@"list"] allKeys]; +} + +/* folder selections */ + +- (BOOL)selectFolder:(id)_url { + NSDictionary *result; + NSString *newFolder; + + newFolder = [_url isKindOfClass:[NSURL class]] + ? [self imap4FolderNameForURL:_url] + : _url; + + if (!alwaysSelect) { + if ([[[self client] selectedFolderName] isEqualToString:newFolder]) + return YES; + } + + result = [[self client] select:newFolder]; + if (![[result valueForKey:@"result"] boolValue]) { + [self errorWithFormat:@"could not select URL: %@: %@", _url, result]; + return NO; + } + + return YES; +} + +- (BOOL)isPermissionDeniedResult:(id)_result { + if ([[_result valueForKey:@"result"] intValue] != 0) + return NO; + + return [[_result valueForKey:@"reason"] + isEqualToString:@"Permission denied"]; +} + +/* folder operations */ + +- (NSArray *)subfoldersForURL:(NSURL *)_url { + NSDictionary *result; + + /* check hierarchy cache */ + + if ((result = [self cachedHierarchyResults]) != nil) + return [self extractSubfoldersForURL:_url fromResultSet:result]; + + [self debugWithFormat:@" no folders cached yet .."]; + + /* fetch _all_ folders */ + + result = [[self client] list:(onlyFetchInbox ? @"INBOX" : @"*") + pattern:@"*"]; + if (![[result valueForKey:@"result"] boolValue]) { + [self errorWithFormat:@"listing of folder failed!"]; + return nil; + } + + /* cache results */ + + if ([result isNotNull]) { + [self cacheHierarchyResults:result]; + if (debugCache) { + [self logWithFormat:@"cached results in entry %@: 0x%08X(%d)", + self, result, [result count]]; + } + } + + /* extract list */ + + return [self extractSubfoldersForURL:_url fromResultSet:result]; +} + +- (NSArray *)allFoldersForURL:(NSURL *)_url { + NSDictionary *result; + + /* check hierarchy cache */ + + if ((result = [self cachedHierarchyResults]) != nil) + return [self extractFoldersFromResultSet:result]; + + [self debugWithFormat:@" no folders cached yet .."]; + + /* fetch _all_ folders */ + + result = [[self client] list:@"INBOX" pattern:@"*"]; + if (![[result valueForKey:@"result"] boolValue]) { + [self logWithFormat:@"ERROR: listing of folder failed!"]; + return nil; + } + + /* cache results */ + + if ([result isNotNull]) { + [self cacheHierarchyResults:result]; + if (debugCache) { + [self logWithFormat:@"cached results in entry %@: 0x%08X(%d)", + self, result, [result count]]; + } + } + + /* extract list */ + return [self extractFoldersFromResultSet:result]; +} + +/* message operations */ + +- (NSArray *)fetchUIDsInURL:(NSURL *)_url qualifier:(id)_qualifier + sortOrdering:(id)_so +{ + /* + sortOrdering can be an NSString, an EOSortOrdering or an array of EOS. + */ + NSDictionary *result; + NSArray *uids; + + /* check cache */ + + uids = [self cachedUIDsForURL:_url qualifier:_qualifier sortOrdering:_so]; + if (uids != nil) { + if (debugCache) [self logWithFormat:@"reusing uid cache!"]; + return [uids isNotNull] ? uids : nil; + } + + /* select folder and fetch */ + + if (![self selectFolder:_url]) + return nil; + + result = [[self client] sort:_so qualifier:_qualifier encoding:@"UTF-8"]; + if (![[result valueForKey:@"result"] boolValue]) { + [self errorWithFormat:@"could not sort contents of URL: %@", _url]; + return nil; + } + + uids = [result valueForKey:@"sort"]; + if (![uids isNotNull]) { + [self errorWithFormat:@"got no UIDs for URL: %@: %@", _url, result]; + return nil; + } + + /* cache */ + + [self cacheUIDs:uids forURL:_url qualifier:_qualifier sortOrdering:_so]; + return uids; +} + +- (NSArray *)fetchUIDs:(NSArray *)_uids inURL:(NSURL *)_url + parts:(NSArray *)_parts +{ + // currently returns a dict?! + /* + Allowed fetch keys: + UID + BODY.PEEK[
]<> + BODY [this is the bodystructure, supported] + BODYSTRUCTURE [not supported yet!] + ENVELOPE [this is a parsed header, but does not include type] + FLAGS + INTERNALDATE + RFC822 + RFC822.HEADER + RFC822.SIZE + RFC822.TEXT + */ + NSDictionary *result; + + if (_uids == nil) + return nil; + if ([_uids count] == 0) + return nil; // TODO: might break empty folders?! return a dict! + + /* select folder */ + + if (![self selectFolder:_url]) + return nil; + + /* fetch parts */ + + // TODO: split uids into batches, otherwise Cyrus will complain + // => not really important because we batch before (in the sort) + // if the list is too long, we get a: + // "* BYE Fatal error: word too long" + + result = [[self client] fetchUids:_uids parts:_parts]; + if (![[result valueForKey:@"result"] boolValue]) { + [self errorWithFormat:@"could not fetch %d uids for url: %@", + [_uids count],_url]; + return nil; + } + + //[self logWithFormat:@"RESULT: %@", result]; + return (id)result; +} + +- (id)fetchURL:(NSURL *)_url parts:(NSArray *)_parts { + // currently returns a dict + NSDictionary *result; + NSString *uid; + + if (![_url isNotNull]) return nil; + + /* select folder */ + + uid = [self imap4FolderNameForURL:_url removeFileName:YES]; + if (![self selectFolder:uid]) + return nil; + + /* fetch parts */ + + uid = [[_url path] lastPathComponent]; + + result = [client fetchUids:[NSArray arrayWithObject:uid] parts:_parts]; + if (![[result valueForKey:@"result"] boolValue]) { + [self errorWithFormat:@"could not fetch url: %@", _url]; + return nil; + } + //[self logWithFormat:@"RESULT: %@", result]; + return (id)result; +} + +- (NSData *)fetchContentOfBodyPart:(NSString *)_partId atURL:(NSURL *)_url { + NSString *key; + NSArray *parts; + id result, fetch, body; + + if (_partId == nil) return nil; + + key = [@"body[" stringByAppendingString:_partId]; + key = [key stringByAppendingString:@"]"]; + parts = [NSArray arrayWithObjects:&key count:1]; + + /* fetch */ + + result = [self fetchURL:_url parts:parts]; + + /* process results */ + + result = [result objectForKey:@"fetch"]; + if ([result count] == 0) { /* did not find part */ + [self errorWithFormat:@"did not find part: %@", _partId]; + return nil; + } + + fetch = [result objectAtIndex:0]; + if ((body = [fetch objectForKey:@"body"]) == nil) { + [self errorWithFormat:@"did not find body in response: %@", result]; + return nil; + } + + if ((result = [body objectForKey:@"data"]) == nil) { + [self errorWithFormat:@"did not find data in body: %@", fetch]; + return nil; + } + return result; +} + +/* message flags */ + +- (NSException *)addOrRemove:(BOOL)_flag flags:(id)_f toURL:(NSURL *)_url { + id result; + + if (![_url isNotNull]) return nil; + if (![_f isNotNull]) return nil; + + if (![_f isKindOfClass:[NSArray class]]) + _f = [NSArray arrayWithObjects:&_f count:1]; + + /* select folder */ + + result = [self imap4FolderNameForURL:_url removeFileName:YES]; + if (![self selectFolder:result]) + return [self errorCouldNotSelectURL:_url]; + + /* store flags */ + + result = [[self client] storeUid:[[[_url path] lastPathComponent] intValue] + add:[NSNumber numberWithBool:_flag] + flags:_f]; + if (![[result valueForKey:@"result"] boolValue]) { + return [self errorForResult:result + text:@"Failed to change flags of IMAP4 message"]; + } + /* result contains 'fetch' key with the current flags */ + return nil; +} +- (NSException *)addFlags:(id)_f toURL:(NSURL *)_u { + return [self addOrRemove:YES flags:_f toURL:_u]; +} +- (NSException *)removeFlags:(id)_f toURL:(NSURL *)_u { + return [self addOrRemove:NO flags:_f toURL:_u]; +} + +- (NSException *)markURLDeleted:(NSURL *)_url { + return [self addOrRemove:YES flags:@"Deleted" toURL:_url]; +} + +- (NSException *)addFlags:(id)_f toAllMessagesInURL:(NSURL *)_url { + id result; + + if (![_url isNotNull]) return nil; + if (![_f isNotNull]) return nil; + + if (![_f isKindOfClass:[NSArray class]]) + _f = [NSArray arrayWithObjects:&_f count:1]; + + /* select folder */ + + if (![self selectFolder:[self imap4FolderNameForURL:_url]]) + return [self errorCouldNotSelectURL:_url]; + + /* fetch all sequence numbers */ + + result = [client searchWithQualifier:nil /* means: ALL */]; + if (![[result valueForKey:@"result"] boolValue]) { + return [self errorForResult:result + text:@"Could not search in IMAP4 folder"]; + } + + result = [result valueForKey:@"search"]; + if ([result count] == 0) /* no messages in there, nothin' to be done */ + return nil; + + /* store flags */ + + result = [[self client] storeFlags:_f forMSNs:result addOrRemove:YES]; + if (![[result valueForKey:@"result"] boolValue]) { + return [self errorForResult:result + text:@"Failed to change flags of IMAP4 message"]; + } + + return nil; +} + +/* posting new data */ + +- (NSException *)postData:(NSData *)_data flags:(id)_f + toFolderURL:(NSURL *)_url +{ + id result; + + if (![_url isNotNull]) return nil; + if (![_f isNotNull]) _f = [NSArray array]; + + if (![_f isKindOfClass:[NSArray class]]) + _f = [NSArray arrayWithObjects:&_f count:1]; + + result = [[self client] append:_data + toFolder:[self imap4FolderNameForURL:_url] + withFlags:_f]; + if (![[result valueForKey:@"result"] boolValue]) + return [self errorForResult:result text:@"Failed to store message"]; + + /* result contains 'fetch' key with the current flags */ + + // TODO: need to flush any caches? + return nil; +} + +/* operations */ + +- (NSException *)expungeAtURL:(NSURL *)_url { + NSString *p; + id result; + + /* select folder */ + + p = [self imap4FolderNameForURL:_url removeFileName:NO]; + if (![self selectFolder:p]) + return [self errorCouldNotSelectURL:_url]; + + /* expunge */ + + result = [[self client] expunge]; + + if (![[result valueForKey:@"result"] boolValue]) { + [self errorWithFormat:@"could not expunge url: %@", _url]; + return nil; + } + //[self logWithFormat:@"RESULT: %@", result]; + return nil; +} + +/* copying and moving */ + +- (NSException *)copyMailURL:(NSURL *)_srcurl toFolderURL:(NSURL *)_desturl { + NSString *srcname, *destname; + unsigned uid; + id result; + + /* names */ + + srcname = [self imap4FolderNameForURL:_srcurl removeFileName:YES]; + uid = [[[_srcurl path] lastPathComponent] unsignedIntValue]; + destname = [self imap4FolderNameForURL:_desturl]; + + /* select source folder */ + + if (![self selectFolder:srcname]) + return [self errorCouldNotSelectURL:_srcurl]; + + /* copy */ + + result = [[self client] copyUid:uid toFolder:destname]; + if (![[result valueForKey:@"result"] boolValue]) + return [self errorForResult:result text:@"Copy operation failed"]; + + // TODO: need to flush some caches? + + return nil; +} + +/* managing folders */ + +- (BOOL)doesMailboxExistAtURL:(NSURL *)_url { + NSString *folderName; + id result; + + /* check in hierarchy cache */ + + if ((result = [self cachedHierarchyResults]) != nil) { + result = [result objectForKey:@"list"]; + return ([result objectForKey:[_url path]] != nil) ? YES : NO; + } + + /* check using IMAP4 select */ + // TODO: we should probably just fetch the whole hierarchy? + + folderName = [self imap4FolderNameForURL:_url]; + result = [[self client] select:folderName]; + if (![[result valueForKey:@"result"] boolValue]) + return NO; + + return YES; +} + +- (id)infoForMailboxAtURL:(NSURL *)_url { + NGImap4MailboxInfo *info; + NSString *folderName; + id result; + + folderName = [self imap4FolderNameForURL:_url]; + result = [[self client] select:folderName]; + if (![[result valueForKey:@"result"] boolValue]) + return [self errorCouldNotSelectURL:_url]; + + info = [[NGImap4MailboxInfo alloc] initWithURL:_url folderName:folderName + selectDictionary:result]; + return [info autorelease]; +} + +- (NSException *)createMailbox:(NSString *)_mailbox atURL:(NSURL *)_url { + NSString *newPath; + id result; + + /* construct path */ + + newPath = [self imap4FolderNameForURL:_url]; + newPath = [newPath stringByAppendingString:[self imap4Separator]]; + newPath = [newPath stringByAppendingString:_mailbox]; + + /* create */ + + result = [[self client] create:newPath]; + if (![[result valueForKey:@"result"] boolValue]) + return [self errorForResult:result text:@"Failed to create folder"]; + + [self flushFolderHierarchyCache]; + // [self debugWithFormat:@"created mailbox: %@: %@", newPath, result]; + return nil; +} + +- (NSException *)deleteMailboxAtURL:(NSURL *)_url { + NSString *path; + id result; + + /* delete */ + + path = [self imap4FolderNameForURL:_url]; + result = [[self client] delete:path]; + if (![[result valueForKey:@"result"] boolValue]) + return [self errorForResult:result text:@"Failed to delete folder"]; + + [self flushFolderHierarchyCache]; +#if 0 + [self debugWithFormat:@"delete mailbox %@: %@", _url, result]; +#endif + return nil; +} + +- (NSException *)moveMailboxAtURL:(NSURL *)_srcurl toURL:(NSURL *)_desturl { + NSString *srcname, *destname; + id result; + + /* rename */ + + srcname = [self imap4FolderNameForURL:_srcurl]; + destname = [self imap4FolderNameForURL:_desturl]; + + result = [[self client] rename:srcname to:destname]; + if (![[result valueForKey:@"result"] boolValue]) + return [self errorForResult:result text:@"Failed to move folder"]; + + [self flushFolderHierarchyCache]; +#if 0 + [self debugWithFormat:@"renamed mailbox %@: %@", _srcurl, result]; +#endif + return nil; +} + +/* ACLs */ + +- (NSDictionary *)aclForMailboxAtURL:(NSURL *)_url { + /* + Returns a mapping of uid => permission strings, eg: + guizmo.g = lrs; + root = lrswipcda; + */ + NSString *folderName; + id result; + + folderName = [self imap4FolderNameForURL:_url]; + result = [[self client] getACL:folderName]; + if (![[result valueForKey:@"result"] boolValue]) { + return (id)[self errorForResult:result + text:@"Failed to get ACL of folder"]; + } + + return [result valueForKey:@"acl"]; +} + +- (NSString *)myRightsForMailboxAtURL:(NSURL *)_url { + NSString *folderName; + id result; + + /* check cache */ + + if ((result = [self cachedMyRightsForURL:_url]) != nil) + return result; + + /* run IMAP4 op */ + + folderName = [self imap4FolderNameForURL:_url]; + result = [[self client] myRights:folderName]; + if (![[result valueForKey:@"result"] boolValue]) { + return (id)[self errorForResult:result + text:@"Failed to get myrights on folder"]; + } + + /* cache results */ + + if ((result = [result valueForKey:@"myrights"]) != nil) + [self cacheMyRights:result forURL:_url]; + return result; +} + +@end /* NGImap4Connection */ diff --git a/sope-mime/NGImap4/NGImap4ConnectionManager.h b/sope-mime/NGImap4/NGImap4ConnectionManager.h new file mode 100644 index 00000000..804a421e --- /dev/null +++ b/sope-mime/NGImap4/NGImap4ConnectionManager.h @@ -0,0 +1,54 @@ +/* + 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. +*/ + +#ifndef __NGImap4ConnectionManager_H__ +#define __NGImap4ConnectionManager_H__ + +#import + +/* + NGImap4ConnectionManager + + This class manages and pools NGImap4Connection objects. +*/ + +@class NSString, NSTimer, NSMutableDictionary, NSURL; +@class NGImap4Connection, NGImap4Client; + +@interface NGImap4ConnectionManager : NSObject +{ + NSMutableDictionary *urlToEntry; + NSTimer *gcTimer; +} + ++ (id)defaultConnectionManager; + +/* client object */ + +- (NGImap4Connection *)connectionForURL:(NSURL *)_url password:(NSString *)_p; + +- (NGImap4Client *)imap4ClientForURL:(NSURL *)_url password:(NSString *)_pwd; + +- (void)flushCachesForURL:(NSURL *)_url; + +@end + +#endif /* __NGImap4ConnectionManager_H__ */ diff --git a/sope-mime/NGImap4/NGImap4ConnectionManager.m b/sope-mime/NGImap4/NGImap4ConnectionManager.m new file mode 100644 index 00000000..f25511e4 --- /dev/null +++ b/sope-mime/NGImap4/NGImap4ConnectionManager.m @@ -0,0 +1,209 @@ +/* + 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. +*/ + +#include "NGImap4ConnectionManager.h" +#include "NGImap4Connection.h" +#include "NGImap4Client.h" +#include "imCommon.h" + +@implementation NGImap4ConnectionManager + +static BOOL debugOn = NO; +static BOOL debugCache = NO; +static BOOL poolingOff = NO; +static NSTimeInterval PoolScanInterval = 5 * 60 /* every five minutes */; + ++ (void)initialize { + NSUserDefaults *ud = [NSUserDefaults standardUserDefaults]; + + debugOn = [ud boolForKey:@"NGImap4EnableIMAP4Debug"]; + debugCache = [ud boolForKey:@"NGImap4EnableIMAP4CacheDebug"]; + poolingOff = [ud boolForKey:@"NGImap4DisableIMAP4Pooling"]; + + if (debugOn) NSLog(@"Note: NGImap4EnableIMAP4Debug is enabled!"); + if (poolingOff) NSLog(@"WARNING: IMAP4 connection pooling is disabled!"); +} + ++ (id)defaultConnectionManager { + static NGImap4ConnectionManager *manager = nil; // THREAD + if (manager == nil) + manager = [[self alloc] init]; + return manager; +} + +- (id)init { + if ((self = [super init])) { + if (!poolingOff) { + self->urlToEntry = [[NSMutableDictionary alloc] initWithCapacity:256]; + } + + self->gcTimer = [[NSTimer scheduledTimerWithTimeInterval: + PoolScanInterval + target:self selector:@selector(_garbageCollect:) + userInfo:nil repeats:YES] retain]; + } + return self; +} + +- (void)dealloc { + if (self->gcTimer) [self->gcTimer invalidate]; + [self->urlToEntry release]; + [self->gcTimer release]; + [super dealloc]; +} + +/* cache */ + +- (id)cacheKeyForURL:(NSURL *)_url { + // protocol, user, host, port + return [NSString stringWithFormat:@"%@://%@@%@:%@", + [_url scheme], [_url user], [_url host], [_url port]]; +} + +- (NGImap4Connection *)entryForURL:(NSURL *)_url { + if (_url == nil) + return nil; + + return [self->urlToEntry objectForKey:[self cacheKeyForURL:_url]]; +} +- (void)cacheEntry:(NGImap4Connection *)_entry forURL:(NSURL *)_url { + if (_entry == nil) _entry = (id)[NSNull null]; + [self->urlToEntry setObject:_entry forKey:[self cacheKeyForURL:_url]]; +} + +- (void)_garbageCollect:(NSTimer *)_timer { + // TODO: scan for old IMAP4 channels + [self debugWithFormat:@"should collect IMAP4 channels (%d active)", + [self->urlToEntry count]]; +} + +- (NGImap4Connection *)connectionForURL:(NSURL *)_url password:(NSString *)_p { + /* + Three cases: + a) not yet connected => create new entry and connect + b) connected, correct password => return cached entry + c) connected, different password => try to recreate entry + */ + NGImap4Connection *entry; + NGImap4Client *client; + + /* check cache */ + + if ((entry = [self entryForURL:_url]) != nil) { + if ([entry isValidPassword:_p]) { + if (debugCache) + [self logWithFormat:@"valid password, reusing cache entry ..."]; + return entry; + } + + /* different password, password could have changed! */ + if (debugCache) + [self logWithFormat:@"different password than cached entry: %@", _url]; + entry = nil; + } + else + [self debugWithFormat:@"no connection cached yet for url: %@", _url]; + + /* try to login */ + + client = [entry isValidPassword:_p] + ? [entry client] + : [self imap4ClientForURL:_url password:_p]; + + if (client == nil) + return nil; + + /* sideeffect of -imap4ClientForURL:password: is to create a cache entry */ + return [self entryForURL:_url]; +} + +/* client object */ + +- (NGImap4Client *)imap4ClientForURL:(NSURL *)_url password:(NSString *)_pwd { + // TODO: move to some global IMAP4 connection pool manager + NGImap4Connection *entry; + NGImap4Client *client; + NSDictionary *result; + + if (_url == nil) + return nil; + + /* check connection pool */ + + if ((entry = [self entryForURL:_url]) != nil) { + if ([entry isValidPassword:_pwd]) { + [self debugWithFormat:@"reused IMAP4 connection for URL: %@", _url]; + return [entry client]; + } + + /* different password, password could have changed! */ + entry = nil; + } + + /* setup connection and attempt login */ + + if ((client = [NGImap4Client clientWithURL:_url]) == nil) + return nil; + + result = [client login:[_url user] password:_pwd]; + if (![[result valueForKey:@"result"] boolValue]) { + [self errorWithFormat: + @"IMAP4 login failed:\n" + @" host=%@, user=%@, pwd=%s\n" + @" url=%@\n base=%@\n base-class=%@)\n" + @" = %@", + [_url host], [_url user], [_pwd length] > 0 ? "yes" : "no", + [_url absoluteString], + [_url baseURL], + NSStringFromClass([[_url baseURL] class]), + client]; + return nil; + } + + [self debugWithFormat:@"created new IMAP4 connection for URL: %@", _url]; + + /* cache connection in pool */ + + entry = [[NGImap4Connection alloc] initWithClient:client + password:_pwd]; + [self cacheEntry:entry forURL:_url]; + [entry release]; entry = nil; + + return client; +} + +- (void)flushCachesForURL:(NSURL *)_url { + NGImap4Connection *entry; + + if ((entry = [self entryForURL:_url]) == nil) /* nothing cached */ + return; + + [entry flushFolderHierarchyCache]; + [entry flushMailCaches]; +} + +/* debugging */ + +- (BOOL)isDebuggingEnabled { + return debugOn; +} + +@end /* NGImap4ConnectionManager */ diff --git a/sope-mime/NGImap4/NGImap4MailboxInfo.h b/sope-mime/NGImap4/NGImap4MailboxInfo.h new file mode 100644 index 00000000..5c131dc5 --- /dev/null +++ b/sope-mime/NGImap4/NGImap4MailboxInfo.h @@ -0,0 +1,60 @@ +/* + Copyright (C) 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. +*/ + +#ifndef __NGImap4MailboxInfo_H__ +#define __NGImap4MailboxInfo_H__ + +#import + +/* + NGImap4MailboxInfo + + Represents the info returned by an IMAP4 select. Use NGImap4Connection to + retrieve the data. +*/ + +@class NSString, NSDate, NSArray, NSURL, NSDictionary; + +@interface NGImap4MailboxInfo : NSObject +{ + NSDate *timestamp; + NSURL *url; + NSString *name; + NSArray *allowedFlags; + NSString *access; + unsigned int recent; +} + +- (id)initWithURL:(NSURL *)_url folderName:(NSString *)_name + selectDictionary:(NSDictionary *)_dict; + +/* accessors */ + +- (NSDate *)timestamp; +- (NSURL *)url; +- (NSString *)name; +- (NSArray *)allowedFlags; +- (NSString *)access; +- (unsigned int)recent; + +@end + +#endif /* __NGImap4MailboxInfo_H__ */ diff --git a/sope-mime/NGImap4/NGImap4MailboxInfo.m b/sope-mime/NGImap4/NGImap4MailboxInfo.m new file mode 100644 index 00000000..6159c736 --- /dev/null +++ b/sope-mime/NGImap4/NGImap4MailboxInfo.m @@ -0,0 +1,101 @@ +/* + Copyright (C) 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. +*/ + +#include "NGImap4MailboxInfo.h" +#include "imCommon.h" + +@implementation NGImap4MailboxInfo + +- (id)initWithURL:(NSURL *)_url folderName:(NSString *)_name + selectDictionary:(NSDictionary *)_dict +{ + if (_dict == nil || (_url == nil && _name == nil)) { + [self release]; + return nil; + } + + if ((self = [super init])) { + self->timestamp = [[NSDate alloc] init]; + self->url = [_url copy]; + self->name = [_name copy]; + self->allowedFlags = [[_dict objectForKey:@"flags"] copy]; + self->access = [[_dict objectForKey:@"access"] copy]; + self->recent = [[_dict objectForKey:@"recent"] unsignedIntValue]; + } + return self; +} +- (id)init { + return [self initWithURL:nil folderName: nil selectDictionary:nil]; +} + +- (void)dealloc { + [self->timestamp release]; + [self->url release]; + [self->name release]; + [self->allowedFlags release]; + [self->access release]; + [super dealloc]; +} + +/* accessors */ + +- (NSDate *)timestamp { + return self->timestamp; +} +- (NSURL *)url { + return self->url; +} +- (NSString *)name { + return self->name; +} +- (NSArray *)allowedFlags { + return self->allowedFlags; +} +- (NSString *)access { + return self->access; +} +- (unsigned int)recent { + return self->recent; +} + +/* description */ + +- (void)appendAttributesToDescription:(NSMutableString *)_ms { + if (self->name) [_ms appendFormat:@" name=%@", self->name]; + if (self->access) [_ms appendFormat:@" access=%@", self->access]; + + if (self->recent != 0) [_ms appendFormat:@" recent=%d", self->recent]; + + [_ms appendFormat:@" flags=%@", + [[self allowedFlags] componentsJoinedByString:@","]]; +} + +- (NSString *)description { + NSMutableString *ms; + + ms = [NSMutableString stringWithCapacity:64]; + [ms appendFormat:@"<0x%08X[%@]:", self, NSStringFromClass([self class])]; + [self appendAttributesToDescription:ms]; + [ms appendString:@">"]; + return ms; +} + +@end /* NGImap4MailboxInfo */ diff --git a/sope-mime/Version b/sope-mime/Version index 31c2fe4f..bd3b377c 100644 --- a/sope-mime/Version +++ b/sope-mime/Version @@ -2,7 +2,7 @@ MAJOR_VERSION:=4 MINOR_VERSION:=5 -SUBMINOR_VERSION:=222 +SUBMINOR_VERSION:=223 # v4.5.214 requires libNGExtensions v4.5.146 # v4.2.149 requires libNGStreams v4.2.34 -- 2.39.5