Commits

Anonymous committed 3de6f13

Created classes for accounts and the account list.
Display accounts in menu

  • Participants
  • Parent commits 786a694

Comments (0)

Files changed (7)

English.lproj/MainMenu.xib

 		</object>
 		<object class="NSMutableArray" key="IBDocument.EditedObjectIDs">
 			<bool key="EncodedWithXMLCoder">YES</bool>
-			<integer value="57"/>
+			<integer value="544"/>
 		</object>
 		<object class="NSArray" key="IBDocument.PluginDependencies">
 			<bool key="EncodedWithXMLCoder">YES</bool>
 							</object>
 						</object>
 					</object>
+					<object class="NSMenuItem" id="10497462">
+						<reference key="NSMenu" ref="649796088"/>
+						<string key="NSTitle">Accounts</string>
+						<string key="NSKeyEquiv"/>
+						<int key="NSMnemonicLoc">2147483647</int>
+						<reference key="NSOnImage" ref="1033313550"/>
+						<reference key="NSMixedImage" ref="310636482"/>
+						<string key="NSAction">submenuAction:</string>
+						<object class="NSMenu" key="NSSubmenu" id="762241924">
+							<string key="NSTitle">Accounts</string>
+							<object class="NSMutableArray" key="NSMenuItems">
+								<bool key="EncodedWithXMLCoder">YES</bool>
+								<object class="NSMenuItem" id="51467641">
+									<reference key="NSMenu" ref="762241924"/>
+									<string key="NSTitle">Edit accounts…</string>
+									<string key="NSKeyEquiv"/>
+									<int key="NSMnemonicLoc">2147483647</int>
+									<reference key="NSOnImage" ref="1033313550"/>
+									<reference key="NSMixedImage" ref="310636482"/>
+								</object>
+								<object class="NSMenuItem" id="925865636">
+									<reference key="NSMenu" ref="762241924"/>
+									<string key="NSTitle">Add new account…</string>
+									<string key="NSKeyEquiv"/>
+									<int key="NSMnemonicLoc">2147483647</int>
+									<reference key="NSOnImage" ref="1033313550"/>
+									<reference key="NSMixedImage" ref="310636482"/>
+								</object>
+							</object>
+						</object>
+					</object>
 					<object class="NSMenuItem" id="713487014">
 						<reference key="NSMenu" ref="649796088"/>
 						<string key="NSTitle">Window</string>
 			<object class="NSCustomObject" id="80551356">
 				<string key="NSClassName">SUUpdater</string>
 			</object>
+			<object class="NSCustomObject" id="609120788">
+				<string key="NSClassName">AccountList</string>
+			</object>
 		</object>
 		<object class="IBObjectContainer" key="IBDocument.Objects">
 			<object class="NSMutableArray" key="connectionRecords">
 					</object>
 					<int key="connectionID">542</int>
 				</object>
+				<object class="IBConnectionRecord">
+					<object class="IBOutletConnection" key="connection">
+						<string key="label">delegate</string>
+						<reference key="source" ref="762241924"/>
+						<reference key="destination" ref="609120788"/>
+					</object>
+					<int key="connectionID">548</int>
+				</object>
+				<object class="IBConnectionRecord">
+					<object class="IBActionConnection" key="connection">
+						<string key="label">createAccount:</string>
+						<reference key="source" ref="609120788"/>
+						<reference key="destination" ref="925865636"/>
+					</object>
+					<int key="connectionID">549</int>
+				</object>
 			</object>
 			<object class="IBMutableOrderedSet" key="objectRecords">
 				<object class="NSArray" key="orderedObjects">
 							<reference ref="379814623"/>
 							<reference ref="586577488"/>
 							<reference ref="1050483726"/>
+							<reference ref="10497462"/>
 						</object>
 						<reference key="parent" ref="0"/>
 					</object>
 						<reference key="object" ref="348820381"/>
 						<reference key="parent" ref="110575045"/>
 					</object>
+					<object class="IBObjectRecord">
+						<int key="objectID">543</int>
+						<reference key="object" ref="10497462"/>
+						<object class="NSMutableArray" key="children">
+							<bool key="EncodedWithXMLCoder">YES</bool>
+							<reference ref="762241924"/>
+						</object>
+						<reference key="parent" ref="649796088"/>
+					</object>
+					<object class="IBObjectRecord">
+						<int key="objectID">544</int>
+						<reference key="object" ref="762241924"/>
+						<object class="NSMutableArray" key="children">
+							<bool key="EncodedWithXMLCoder">YES</bool>
+							<reference ref="51467641"/>
+							<reference ref="925865636"/>
+						</object>
+						<reference key="parent" ref="10497462"/>
+					</object>
+					<object class="IBObjectRecord">
+						<int key="objectID">545</int>
+						<reference key="object" ref="51467641"/>
+						<reference key="parent" ref="762241924"/>
+					</object>
+					<object class="IBObjectRecord">
+						<int key="objectID">546</int>
+						<reference key="object" ref="925865636"/>
+						<reference key="parent" ref="762241924"/>
+					</object>
+					<object class="IBObjectRecord">
+						<int key="objectID">547</int>
+						<reference key="object" ref="609120788"/>
+						<reference key="parent" ref="0"/>
+					</object>
 				</object>
 			</object>
 			<object class="NSMutableDictionary" key="flattenedProperties">
 					<string>29.WindowOrigin</string>
 					<string>29.editorWindowContentRectSynchronizationRect</string>
 					<string>295.IBPluginDependency</string>
+					<string>296.IBEditorWindowLastContentRect</string>
 					<string>296.IBPluginDependency</string>
 					<string>296.editorWindowContentRectSynchronizationRect</string>
 					<string>297.IBPluginDependency</string>
 					<string>532.IBPluginDependency</string>
 					<string>534.IBPluginDependency</string>
 					<string>537.IBPluginDependency</string>
-					<string>540.IBPluginDependency</string>
 					<string>541.IBPluginDependency</string>
+					<string>543.IBPluginDependency</string>
+					<string>544.IBEditorWindowLastContentRect</string>
+					<string>544.IBPluginDependency</string>
+					<string>545.IBPluginDependency</string>
+					<string>546.IBPluginDependency</string>
+					<string>547.IBPluginDependency</string>
 					<string>56.IBPluginDependency</string>
 					<string>56.ImportedFromIB2</string>
 					<string>57.IBEditorWindowLastContentRect</string>
 					<integer value="1"/>
 					<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
 					<integer value="1"/>
-					<string>{{784, 313}, {194, 73}}</string>
+					<string>{{534, 440}, {194, 73}}</string>
 					<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
 					<integer value="1"/>
 					<string>{{525, 802}, {197, 73}}</string>
-					<string>{{404, 386}, {336, 20}}</string>
+					<string>{{330, 513}, {417, 20}}</string>
 					<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
 					<integer value="1"/>
 					<string>{74, 862}</string>
 					<string>{{11, 977}, {478, 20}}</string>
 					<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
+					<string>{{484, 470}, {231, 43}}</string>
 					<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
 					<string>{{475, 832}, {234, 43}}</string>
 					<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
 					<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
 					<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
 					<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
+					<string>{{534, 470}, {183, 43}}</string>
+					<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
+					<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
+					<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
+					<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
 					<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
 					<integer value="1"/>
-					<string>{{416, 183}, {229, 203}}</string>
+					<string>{{342, 310}, {229, 203}}</string>
 					<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
 					<integer value="1"/>
 					<string>{{23, 794}, {245, 183}}</string>
 				</object>
 			</object>
 			<nil key="sourceID"/>
-			<int key="maxID">542</int>
+			<int key="maxID">549</int>
 		</object>
 		<object class="IBClassDescriber" key="IBDocument.Classes">
 			<object class="NSMutableArray" key="referencedPartialClassDescriptions">
 				<bool key="EncodedWithXMLCoder">YES</bool>
 				<object class="IBPartialClassDescription">
+					<string key="className">AccountList</string>
+					<string key="superclassName">NSObject</string>
+					<object class="NSMutableDictionary" key="actions">
+						<bool key="EncodedWithXMLCoder">YES</bool>
+						<object class="NSArray" key="dict.sortedKeys">
+							<bool key="EncodedWithXMLCoder">YES</bool>
+							<string>createAccount:</string>
+							<string>openAccount:</string>
+						</object>
+						<object class="NSMutableArray" key="dict.values">
+							<bool key="EncodedWithXMLCoder">YES</bool>
+							<string>id</string>
+							<string>id</string>
+						</object>
+					</object>
+					<object class="IBClassDescriptionSource" key="sourceIdentifier">
+						<string key="majorKey">IBProjectSource</string>
+						<string key="minorKey">Source/AccountList.h</string>
+					</object>
+				</object>
+				<object class="IBPartialClassDescription">
 					<string key="className">AppController</string>
 					<string key="superclassName">NSObject</string>
 					<object class="NSMutableDictionary" key="actions">
 				<string>application/sieve</string>
 			</dict>
 		</dict>
+		<dict>
+			<key>UTTypeConformsTo</key>
+			<array>
+				<string>com.apple.property-list</string>
+			</array>
+			<key>UTTypeIdentifier</key>
+			<string>net.dergraf.sieve.sieve-account</string>
+			<key>UTTypeTagSpecification</key>
+			<dict>
+				<key>public.filename-extension</key>
+				<array>
+					<string>sieveAccount</string>
+				</array>
+			</dict>
+		</dict>
 	</array>
 </dict>
 </plist>

Sieve.xcodeproj/project.pbxproj

 		268B28F4121EB0B00046E709 /* ConnectionController.m in Sources */ = {isa = PBXBuildFile; fileRef = 268B28F3121EB0B00046E709 /* ConnectionController.m */; };
 		26B5D31C1234F4C100C065F8 /* AsyncSocket.strings in Resources */ = {isa = PBXBuildFile; fileRef = 26B5D3181234F4C100C065F8 /* AsyncSocket.strings */; };
 		26B5D31D1234F4C100C065F8 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 26B5D31A1234F4C100C065F8 /* Localizable.strings */; };
+		26C153FB12379AC3002A242E /* AccountList.m in Sources */ = {isa = PBXBuildFile; fileRef = 26C153FA12379AC3002A242E /* AccountList.m */; };
+		26C1546C1237C02B002A242E /* Account.m in Sources */ = {isa = PBXBuildFile; fileRef = 26C1546B1237C02B002A242E /* Account.m */; };
 		26F2AE121212972C00458B80 /* NSBezierPath_AMShading.m in Sources */ = {isa = PBXBuildFile; fileRef = 26F2ADF21212972C00458B80 /* NSBezierPath_AMShading.m */; };
 		26F2AE131212972C00458B80 /* NSString_AITruncation.m in Sources */ = {isa = PBXBuildFile; fileRef = 26F2ADF41212972C00458B80 /* NSString_AITruncation.m */; };
 		26F2AE141212972C00458B80 /* PSMAdiumTabStyle.m in Sources */ = {isa = PBXBuildFile; fileRef = 26F2ADF61212972C00458B80 /* PSMAdiumTabStyle.m */; };
 		268B28F3121EB0B00046E709 /* ConnectionController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ConnectionController.m; sourceTree = "<group>"; };
 		26B5D3191234F4C100C065F8 /* English */ = {isa = PBXFileReference; fileEncoding = 10; lastKnownFileType = text.plist.strings; name = English; path = English.lproj/AsyncSocket.strings; sourceTree = "<group>"; };
 		26B5D31B1234F4C100C065F8 /* English */ = {isa = PBXFileReference; fileEncoding = 10; lastKnownFileType = text.plist.strings; name = English; path = English.lproj/Localizable.strings; sourceTree = "<group>"; };
+		26C153F912379AC3002A242E /* AccountList.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AccountList.h; sourceTree = "<group>"; };
+		26C153FA12379AC3002A242E /* AccountList.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AccountList.m; sourceTree = "<group>"; };
+		26C1546A1237C02B002A242E /* Account.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = Account.h; sourceTree = "<group>"; };
+		26C1546B1237C02B002A242E /* Account.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = Account.m; sourceTree = "<group>"; };
 		26F2ADF11212972C00458B80 /* NSBezierPath_AMShading.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = NSBezierPath_AMShading.h; sourceTree = "<group>"; };
 		26F2ADF21212972C00458B80 /* NSBezierPath_AMShading.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = NSBezierPath_AMShading.m; sourceTree = "<group>"; };
 		26F2ADF31212972C00458B80 /* NSString_AITruncation.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = NSString_AITruncation.h; sourceTree = "<group>"; };
 				268B28F3121EB0B00046E709 /* ConnectionController.m */,
 				267509B11235574D00B8E26D /* CreateAccountWindowController.h */,
 				267509B21235574D00B8E26D /* CreateAccountWindowController.m */,
+				26C153F912379AC3002A242E /* AccountList.h */,
+				26C153FA12379AC3002A242E /* AccountList.m */,
+				26C1546A1237C02B002A242E /* Account.h */,
+				26C1546B1237C02B002A242E /* Account.m */,
 			);
 			path = Source;
 			sourceTree = "<group>";
 				267508D212352FCC00B8E26D /* ServiceLookup.m in Sources */,
 				267509B31235574D00B8E26D /* CreateAccountWindowController.m in Sources */,
 				2661BD6A123584A7001CBBC5 /* PFMoveApplication.m in Sources */,
+				26C153FB12379AC3002A242E /* AccountList.m in Sources */,
+				26C1546C1237C02B002A242E /* Account.m in Sources */,
 			);
 			runOnlyForDeploymentPostprocessing = 0;
 		};
+/* Copyright (c) 1010 Sven Weidauer
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated 
+ * documentation files (the "Software"), to deal in the Software without restriction, including without limitation 
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and 
+ * to permit persons to whom the Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the 
+ * Software.
+ * 
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO 
+ * THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 
+ * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 
+ */
+
+#import <Cocoa/Cocoa.h>
+
+
+@interface Account : NSObject {
+    NSURL *accountFileURL;
+    NSString *accountName;
+    NSString *host;
+    NSString *user;
+    BOOL tls;
+    unsigned port;
+    NSImage *icon;
+}
+
+@property (readwrite, copy) NSString *accountName;
+@property (readwrite, copy) NSString *host;
+@property (readwrite, copy) NSString *user;
+@property (readwrite, assign) BOOL tls;
+@property (readwrite, assign) unsigned port;
+@property (readwrite, copy) NSImage *icon;
+@property (readonly) NSURL *accountURL;
+
++ (Account *) readFromURL: (NSURL *) url;
+
+- (BOOL) saveError: (NSError **) outError;
+
+@end
+/* Copyright (c) 1010 Sven Weidauer
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated 
+ * documentation files (the "Software"), to deal in the Software without restriction, including without limitation 
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and 
+ * to permit persons to whom the Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the 
+ * Software.
+ * 
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO 
+ * THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 
+ * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 
+ */
+
+#import "Account.h"
+#import "AccountList.h"
+
+@interface Account ()
+
++ (NSImage *) defaultAccountIcon;
+- initWithAccountFileURL: (NSURL *) url;
+
+@property (copy, readwrite) NSURL *accountFileURL;
+
+@end
+
+
+@implementation Account
+
+@synthesize accountFileURL;
+@synthesize accountName;
+@synthesize host;
+@synthesize user;
+@synthesize tls;
+@synthesize port;
+@synthesize icon;
+
++ (NSImage *) defaultAccountIcon;
+{
+    static NSImage *icon = nil;
+    
+    @synchronized( self ) {
+        if (nil == icon) {
+        }
+    }
+    
+    return icon;
+}
+
+static NSString * const kHostKey = @"host";
+static NSString * const kUserKey = @"user";
+static NSString * const kTLSKey = @"tls";
+static NSString * const kPortKey = @"port";
+
+- initWithAccountFileURL: (NSURL *) url;
+{
+    self = [super init];
+    if (nil == self) {
+        return nil;
+    }
+
+    NSDictionary *dict = [NSDictionary dictionaryWithContentsOfURL: url];
+    if (nil == dict) {
+        [self release];
+        return nil;
+    }
+
+    [self setAccountFileURL: url];
+    
+    [self setHost: [dict objectForKey: kHostKey]];
+    if (nil == host) {
+        [self release];
+        return nil;
+    }
+    
+    [self setUser: [dict objectForKey: kUserKey]];
+    
+    NSNumber *tlsNumber = [dict objectForKey: kTLSKey];
+    if (nil != tlsNumber) {
+        [self setTls: [tlsNumber boolValue]];
+    } else {
+        [self setTls: YES];
+    }
+
+    NSNumber *portNumber = [dict objectForKey: kPortKey];
+    if (nil != portNumber) {
+        [self setPort: [portNumber unsignedIntValue]];        
+    } else {
+        [self setPort: 2000];
+    }
+
+    NSString *name = nil;
+    if (![url getResourceValue: &name forKey: NSURLLocalizedNameKey error: NULL] || nil == name) {
+        name = [[url lastPathComponent] stringByDeletingPathExtension];
+    }
+    [self setAccountName: name];
+    
+    NSImage *customFileIcon = nil;
+    if ([url getResourceValue: &customFileIcon forKey: NSURLCustomIconKey error: NULL] && nil != customFileIcon) {
+        [self setIcon: customFileIcon];
+    }    
+    
+    return self;
+}
+
++ (Account *) readFromURL: (NSURL *) url;
+{
+    return [[[self alloc] initWithAccountFileURL: url] autorelease];
+}
+
+- (NSImage *) icon;
+{
+    if (nil == icon) return [[self class] defaultAccountIcon];
+    else return icon;
+}
+
+- (BOOL) saveError: (NSError **) outError;
+{
+    NSDictionary *dict = [NSDictionary dictionaryWithObjectsAndKeys: host, kHostKey, 
+                          user != nil ? user : @"", kUserKey, 
+                          [NSNumber numberWithBool: tls], kTLSKey,
+                          [NSNumber numberWithUnsignedInt: port], kPortKey,
+                          nil];
+    
+
+    NSError *error = nil;
+    NSData *saveData = [NSPropertyListSerialization dataWithPropertyList: dict format: NSPropertyListXMLFormat_v1_0 
+                                                                 options: 0 error: &error];
+    if (nil == saveData) {
+        if (NULL != outError) {
+            *outError = error;   
+        }
+        return NO;
+    }
+    
+    NSURL *saveURL = accountFileURL;
+    if (nil == saveURL) {
+        NSString *fileName = [accountName stringByAppendingPathExtension: @"sieveAccount"];
+        saveURL = [[[AccountList sharedAccountList] accountsFolder] URLByAppendingPathComponent: fileName];
+    }
+    
+    if (![saveData writeToURL: saveURL options: 0 error: &error]) {
+        if (NULL != outError) {
+            *outError = error;
+        }
+        return NO;
+    }
+    
+    [self setAccountFileURL: saveURL];
+    [saveURL setResourceValue: [NSNumber numberWithBool: YES] forKey: NSURLHasHiddenExtensionKey error: NULL];
+    
+    return YES;
+}
+
+- (NSURL *) accountURL;
+{
+    NSString *hostPart = host;
+    if (nil != user && ![user isEqualToString: @""]) {
+        hostPart = [user stringByAppendingFormat: @"@%@", host];
+    }
+    return [NSURL URLWithString: [NSString stringWithFormat: @"sieve://%@:%u/", hostPart, port]];
+}
+
+@end

Source/AccountList.h

+/* Copyright (c) 1010 Sven Weidauer
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated 
+ * documentation files (the "Software"), to deal in the Software without restriction, including without limitation 
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and 
+ * to permit persons to whom the Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the 
+ * Software.
+ * 
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO 
+ * THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 
+ * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 
+ */
+
+#import <Cocoa/Cocoa.h>
+
+extern NSString * const kAccountUTI;
+extern NSString * const kAccountsFolderDefaultsKey;
+
+@interface AccountList : NSObject < NSMenuDelegate > {
+    NSURL *accountsFolder;
+    NSString *observedPath;
+    
+    NSMutableArray *accounts;
+    
+    BOOL updateMenu;
+}
+
+@property (readonly, copy) NSURL *accountsFolder;
+
++ (NSURL *) accountsFolder;
+
++ (AccountList *) sharedAccountList;
+
+- (NSArray *) accounts;
+- (void) setAccounts: (NSArray *) newAccounts;
+
+- (IBAction) openAccount: (id) sender;
+- (IBAction) createAccount: (id) sender;
+
+@end

Source/AccountList.m

+/* Copyright (c) 1010 Sven Weidauer
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated 
+ * documentation files (the "Software"), to deal in the Software without restriction, including without limitation 
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and 
+ * to permit persons to whom the Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the 
+ * Software.
+ * 
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO 
+ * THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 
+ * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 
+ */
+
+#import "AccountList.h"
+#import "Account.h"
+#import "ConnectionController.h"
+#import "CreateAccountWindowController.h"
+
+NSString * const kAccountsFolderDefaultsKey = @"accountsFolder";
+
+@interface AccountList ()
+
+@property (readwrite, copy) NSURL *accountsFolder;
+@property (readwrite, copy, nonatomic) NSString *observedPath;
+
+- (void) scanAccountsDirectory;
+
+@end
+
+
+@implementation AccountList
+
+@synthesize accountsFolder;
+@synthesize observedPath;
+
++ (void) initialize;
+{
+    NSArray *paths = NSSearchPathForDirectoriesInDomains( NSApplicationSupportDirectory, NSUserDomainMask, NO );
+    if ([paths count] >= 1) {
+        NSString *supportDirectory = [[paths objectAtIndex: 0] stringByAppendingPathComponent: [[NSBundle mainBundle] bundleIdentifier]];
+        NSDictionary *defaults = [NSDictionary dictionaryWithObjectsAndKeys: [supportDirectory stringByAppendingPathComponent: @"Accounts"], kAccountsFolderDefaultsKey, nil];
+        
+        [[NSUserDefaults standardUserDefaults] registerDefaults: defaults];
+    }
+}
+
++ (NSURL *) accountsFolder;
+{
+    NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
+    NSURL *result = [[defaults URLForKey: kAccountsFolderDefaultsKey] fileReferenceURL];
+
+    NSString *path = [result path];
+    if (nil == path) {
+        path = [[[defaults volatileDomainForName: NSRegistrationDomain] objectForKey: kAccountsFolderDefaultsKey] stringByExpandingTildeInPath];
+    }
+
+    NSFileManager *fm = [NSFileManager defaultManager];
+    if (![fm fileExistsAtPath: path]) {
+        NSError *outError = nil;
+        if (![fm createDirectoryAtPath: path withIntermediateDirectories: YES attributes: nil error: &outError]) {
+            NSAlert *alert = [NSAlert alertWithError: outError];
+            [alert runModal];
+            return nil;
+        }
+        
+        result = [[NSURL fileURLWithPath: path isDirectory: YES] fileReferenceURL];
+        [defaults setURL: result forKey: kAccountsFolderDefaultsKey];
+    }
+    
+    return result;
+}
+
+static AccountList *sharedAccountList = nil;
+
++ (AccountList *) sharedAccountList;
+{
+    @synchronized (self) {
+        if (nil == sharedAccountList) {
+            [[self alloc] init];
+        }
+        return sharedAccountList;
+    }
+}
+
+- init;
+{
+    self = [super init];
+
+    @synchronized ([self class]) {
+        if (nil != sharedAccountList) {
+            [self release];
+            self = sharedAccountList;
+        } else if (nil != self) {
+            sharedAccountList = self;
+
+            [self setAccountsFolder: [[self class] accountsFolder]];
+        }
+    }
+  
+    return self;
+}
+
+- (id) copyWithZone: (NSZone *) zone;
+{
+    return self;
+}
+
+- (id) retain;
+{
+    return self;
+}
+
+- (id) autorelease;
+{
+    return self;
+}
+
+- (void) release;
+{
+}
+
+- (NSUInteger) retainCount;
+{
+    return NSUIntegerMax;
+}
+
+- (void) setAccountsFolder: (NSURL *) folder;
+{
+    if (accountsFolder != folder) {
+        [accountsFolder release];
+        accountsFolder = [folder copy];
+        
+        [self setObservedPath: [accountsFolder path]];
+        
+        if (nil != accountsFolder) {
+            [[NSUserDefaults standardUserDefaults] setURL: [accountsFolder fileReferenceURL] forKey: kAccountsFolderDefaultsKey];
+        }
+    }
+}
+
+- (void) setObservedPath: (NSString *) newObservedPath;
+{
+    if (newObservedPath != observedPath) {
+        if (nil != observedPath) {
+            // TODO: stop observing observedPath
+        }
+
+        [observedPath release];
+        observedPath = [newObservedPath copy];
+
+        if (nil != observedPath) {
+            // TODO: start observing observedPath
+            [self scanAccountsDirectory];
+        }
+    }
+}
+
+NSString * const kAccountUTI = @"net.dergraf.sieve.sieve-account";
+
+- (void) scanAccountsDirectory;
+{
+    NSDirectoryEnumerator *dir = [[NSFileManager defaultManager] enumeratorAtURL: accountsFolder 
+                                                      includingPropertiesForKeys: [NSArray arrayWithObjects: NSURLLocalizedNameKey, NSURLTypeIdentifierKey, NSURLCustomIconKey, nil]
+                                                                         options: NSDirectoryEnumerationSkipsPackageDescendants | NSDirectoryEnumerationSkipsHiddenFiles
+                                                                    errorHandler: nil];
+    NSMutableArray *allAccounts = [NSMutableArray array];
+    for (NSURL *item in dir) {
+        NSString *uti = nil;
+        if (![item getResourceValue: &uti forKey: NSURLTypeIdentifierKey error: NULL]) {
+            continue;
+        }
+        
+        if (UTTypeEqual( (CFStringRef)uti, (CFStringRef)kAccountUTI )) {
+            [allAccounts addObject: [Account readFromURL: item]];
+        }
+    }
+    
+    [self setAccounts: allAccounts];
+}
+
+- (NSArray *) accounts;
+{
+    return accounts;
+}
+
+- (void) setAccounts: (NSArray *) newAccounts;
+{
+    if (accounts != newAccounts) {
+        [accounts release];
+        accounts = [newAccounts mutableCopy];
+        updateMenu = YES;
+    }
+}
+
+- (IBAction) openAccount: (id) sender;
+{
+    NSURL *url = [[sender representedObject] accountURL];
+    [[ConnectionController sharedConnectionController] openURL: url];
+}
+
+- (IBAction) createAccount: (id) sender;
+{
+    CreateAccountWindowController *wc = [[CreateAccountWindowController alloc] init];
+    [wc showWindow: sender];
+}
+
+#pragma mark -
+#pragma mark NSMenuDelegate
+
+enum {
+    kDefaultTag = 0,
+    kSeparatorTag = 1,
+    kBookmarkTag,
+};
+
+- (void) menuNeedsUpdate: (NSMenu *)menu;
+{
+    if (updateMenu) {
+        updateMenu = NO;
+        
+        NSMenuItem *separator = [menu itemWithTag: kSeparatorTag];
+        if (nil == separator && [accounts count] > 0) {
+            separator = [NSMenuItem separatorItem];
+            [separator setTag: kSeparatorTag];
+            [menu addItem: separator];
+        }
+        
+        const NSInteger count = [menu numberOfItems];
+        NSInteger firstItem = [menu indexOfItemWithTag: kBookmarkTag];
+        if (-1 == firstItem) firstItem = count;
+        
+        for (Account *account in accounts) {
+            NSMenuItem *item = nil;
+            if (firstItem < count) {
+                item = [menu itemAtIndex: firstItem];
+                [item setTitle: [account accountName]];
+            } else {
+                item = [[[NSMenuItem alloc] initWithTitle: [account accountName] action: @selector(openAccount:) keyEquivalent: @""] autorelease];
+                [item setTag: kBookmarkTag];
+                [item setTarget: self];
+                [menu addItem: item];
+            }
+            [item setRepresentedObject: account];
+            [item setImage: [account icon]];
+            ++firstItem;
+        }
+        
+        // Remove all extra menu items...
+        for (int i = count - 1; i >= firstItem; --i) {
+            NSLog( @"remove %d", i );
+            [menu removeItemAtIndex: i];
+        }
+    }
+}
+
+@end