Mail Enhancement

I like partitions. Got a bunch of ’em on my laptop: several smaller ones, and a large one for data. It’s an easy way to test different OS versions.

Easy, at least, until the OS started taking up so much damn space! Now, I regularly fill up the smaller partitions. One solution is to resize them, an issue I’ll get to later.

But another solution is to move the large personal data stores that are normally replicated on each OS partition to the data partition, where they belong.

For example: my email. Mail normally stores its data in ~/Library/Mail:

POP:
Apple Mail POP account advanced tab, showing Account Directory setting

IMAP:
Apple Mail IMAP account advanced tab, showing Account Directory setting

But I’m never prompted where I want to put this Account Directory, it always defaults to ~/Library/Mail, and as you can see, it’s dimmed, so I can’t change it in the user interface once the account has been created. Well, there may be a way, but it probably involves throwing away all the messages I already have, which I don’t want.

All the information on my accounts is stored in the plist file at ~/Library/Preferences/com.apple.mail.plist. Ah, that’s easy, I think to myself, I’ll just open that file in a text editor and I’ll….

Bzzzt! (Notice how I like that word?) The plist is in binary format: I wouldn’t recommend trying to modify it with TextEdit.

For this, I’ll need to open the plist file in Property List Editor, which is available, after an Xcode Tools install, at /Developer/Applications/Utilities/Property List Editor.app.

I use the "Dump" button in the upper right-hand corner of the Editor window to get a dump of the plist file as text, and then copy that text to TextEdit (or a better editor such as BBEdit) and save it off somewhere.

Now, I need to search on every instance of ~/Library/Mail/ in that file, and change that path to the new path I want, /Volumes/Data/Mail/. The primary location for this in the plist is: MailAccounts → <account dictionary> → AccountPath.

What’s surprising is that this isn’t the only location where I find that path. In fact, it seems that the full path in the file system to the account directory is what’s used to refer to that account throughout the plist. I make sure to change them all.

The plist also contains the path ~/Library/Mail Downloads. I change that to /Volumes/Data/Mail/Downloads, just to keep all my Mail data in the same Mail folder.

Now that I’m done with the find-and-replace steps, I close Mail and back up all its data. Very important!

Next, I go to the ~/Library/Mail folder. I find the folder called Mailboxes, if it exists, and copy it to the new location.

I find every folder of the form ACCOUNTTYPE-username@account.domain.com, where ACCOUNTTYPE is IMAP or POP, etc., and account.domain.com is your incoming mail server, and copy them over as well. I don’t copy everything in ~/Library/Mail.

I copy the ~/Library/Mail Downloads folder into the new Mail folder, and rename it just Downloads, to match what I specified in the new plist, above.

Finally, while Mail still is closed, I replace the old binary plist (which I also backed up) with my newly edited, text plist. I say a little prayer, and reopen Mail.

It works!

Apple Mail changed Account Directory setting

Finally, once I’m sure everything is working, I get rid of the Mail folders on my OS partition. I follow all the same steps as listed above for my other OS partitions (except for populating /Volumes/Data/Mail, which I do only once with the most up-to-date data), and I’m done.

Feedback #1 If all I have is IMAP accounts, why not just go ahead and never store anything locally? Problem solved!

For one, IMAP is slow enough as it is: if I have to download all my messages from the server every time I open Mail, it’ll go even slower! For two, Mail tells me it won’t be able to check for spam under that configuration, though I haven’t verified this.

Feedback #2 If you do steps X, Y, and Z, you can get that widget to activate, and move the storage. Why not just do that?

As I said above, there may be a way to change the Account Directory value in the UI, but I suspect it only lets you do that if you empty your local storage, which I don’t want to do. If you do find some other series of steps that don’t have that drawback, let me know!

Also, remember that Mailboxes folder I moved? That holds all the local mailboxes I made, via Mailbox→New Mailbox…. I can’t move that to my central location without delving into the Mail plist as I’ve described above.

Update: There are a couple of good comments that describe both simpler and yet more all-or-nothing solutions than the one I proposed. Have a look!

Also, I originally advised moving the folder Icons, but it’s both not where I said it was, and you shouldn’t move it. It’s not at ~/Library/Mail/, it’s just at ~/Library/, and that’s a good thing, because the icons are used by more than one application. So leave ’em there!

Authority Issues II: The Dialog That Wouldn’t Die

Last time around, I talked about the Security framework. It is a set of “beneath the hood” C-based APIs you can use from Carbon or Cocoa.

In addition, there’s the Security Interface framework, which is a Cocoa framework of standard security UI widgets.

One of the widgets in this framework is the SFAuthorizationView, which looks like so in its unauthorized and authorized states:
SFAuthorizationView, locked iconSFAuthorizationView, unlocked icon

You tell it which right you want it to use, and it manages an AuthorizationRef for that right. It’s a very useful widget, but it has a gotcha.

You add an SFAuthorizationView to a window in your nib file by doing the following:

1) Drag the SecurityInterface/SFAuthorizationView.h header to your nib.

2) Make a custom view.

3) Change that custom view’s custom class to SFAuthorizationView.

Here’s some code for a delegate class for such a window. It assumes the delegate has an SFAuthorizationView outlet called “authorizationView”:

- (void)awakeFromNib
{
   [authorizationView setString:"right.to.party"];
   [authorizationView updateStatus:self];

   [authorizationView setFlags:
      kAuthorizationFlagInteractionAllowed |
      kAuthorizationFlagExtendRights];

   [authorizationView setAutoupdate:YES];
}

The first two messages are necessary to set up the authorization view. Without a specified right, the view can’t function. Without the updateStatus: message, it won’t show the lock icon correctly.

You’ll notice that the value for the setFlags: message is the same value I used for the second AuthoriaztionCreate)() function call in my last post. It serves the same purpose here that it did there.

But it’s the last call that’s the gotcha. You send the message setAutoupdate:YES if you want the authorization view to automatically change its appearance and behavior when, for example, an authorization expires, which they normally do after 5 minutes. So far so good.

But if that is combined with the flag kAuthorizationFlagInteractionAllowed, and if a user decides to cancel out of an authorization dialog instead of typing in a valid password…the then dialog comes back. Cancel it again, it comes back again. If you don’t have the password — say, for example, you’re not an adminstrative user for the Mac in question — the only way to get the dialog-that-won’t-die to go away is to force-quit the entire application.

Not good. There are two workarounds for this gotcha:

1) Don’t use kAuthorizationFlagInteractionAllowed. This means that when an update occurs, for example an authorization times out, the user won’t be prompted to authorize again immediately. Instead, the lock icon will just go ahead and lock itself. There isn’t a “nyah nyah” sound, but there might as well be.

2) Don’t set your view to autoupdate. You might do this if you’re using a right that, say, never expires.

For anyone using or thinking of using the SFAuthorizationView widget, I hope this helps.

Authority Issues: Gotchas with the Security Framework

You see those pesky authorization dialogs every so often, right? "Finder requires that you type your password." "BBEdit requires that you type your password." This means the application involved is using the Security framework to perform an action with, say, administrative privileges that it wouldn’t normally have.

If you turn down the Details disclosure triangle in such a dialog, you see the name of the app and an arcane Requested Right string.

These rights should be added to what’s called the "policy database," stored in the file /etc/authorization, when the application that wants to use them is first installed. For a full explanation, see Performing Privileged Operations With Authorization Services, but for this post, I’m interested in the API you use to add those rights.

Here’s the example given in the AuthorizationDB.h header in the Security framework.

OSStatus status =
   AuthorizationRightSet(
      NULL,
      "com.ifoo.ifax.send",
      CFSTR(kAuthorizationRuleIsAdmin),
      CFSTR("Authorize sending of a fax"),
      NULL,
      NULL);

Looks reasonable, right?

Bzzzt. Sorry! If you actually try to run this code, at least on Mac OS X 10.4 "Tiger," you get a −2147418108 error. What’s a −2147418108 error? No clue. But it’s not good.

The first parameter is of type AuthorizationRef. You get a valid authorization reference if the user types her password into one of those dialogs, but you don’t need special permission to add a new right to the policy database. Hence the null value in the example. But it doesn’t work.

Here’s what you need to do instead:

AuthorizationRef authorization = NULL;

OSStatus result =
   AuthorizationCreate(
      NULL, NULL,
      kAuthorizationFlagDefaults,
      &authorization);

if (result == noErr)
{
   result =
      AuthorizationRightSet(
         authorization,
         "com.ifoo.ifax.send",
         CFSTR(kAuthorizationRuleAuthenticateAsAdmin),
         CFSTR("Authorize sending of a fax"),
         NULL,
         NULL);

   AuthorizationFree(
      authorization,
      kAuthorizationFlagDefaults);
}

AuthorizationCreate() actually has a whole bunch of options you can use, but here I’m using it with minimal values to create a blank, good-for-nothing AuthorizationRef. Even so, that’s good enough for AuthorizationRightSet(). With this code, there’s no −2147418108 error.

So we’re done? Not yet!

You’ll notice above that I changed the third, "rule" parameter of AuthorizationRightSet() from the kAuthorizationRuleIsAdmin in the original sample to kAuthorizationRuleAuthenticateAsAdmin. With that rule, you are shown an authorization dialog when I use AuthorizationCreate() like so:

AuthorizationItem myItems =
   { "com.ifoo.ifax.send", 0, NULL, 0 };

AuthorizationRights myRights =
   { 0, &myItems };

AuthorizationFlags myFlags =
   kAuthorizationFlagInteractionAllowed |
   kAuthorizationFlagExtendRights;

AuthorizationRef authorization = NULL;

OSStatus result =
   AuthorizationCreate(&myRights, kAuthorizationEmptyEnvironment,
      myFlags, &authorization);

This call’s a lot more complicated, eh? But it boils down to trying to get a valid authorization for the right we added.

Here’s the dialog you see in response:

Authorization dialog with erroneous caption

Oops! If you fill in the fourth, "description key" parameter of AuthorizationRightSet() with a string as shown in the header example, that "description key" is concatenated with the default system prompt in any authorization dialog that refers to that right.

Passing NULL for the "description key" parameter uses just the default system prompt, which works much better.

Now we’re done.

Banana Split: Overcoming Problems with NSSplitView

There are a bunch of bugs or misbehaviors in the current (10.4) version of NSSplitView. Here are the ones I’ve needed to solve:

(1) While the split view respects the minimum and maximum coordinates if you drag the divider around yourself, it doesn’t respect them if you’ve set up the view to resize with the window, and you shrink the window.

(2) If you collapse a subview, any view in it that has keystroke focus retains it. You can still type in invisible text fields, navigate invisible tables, etc. Plus, all the text fields, tables, etc. inside the collapsed subview can still be tabbed through.

(3) The coordinates of the subviews are not automatically saved and restored, the way the column sizes and positions in a NSTableView are.

In my recent project, I fixed all these problems with delegate methods. If you implement splitView:resizeSubviewsWithOldSize: and do the resizing yourself in the problem case, you can fix (1). If you implement splitViewDidResizeSubviews:, you can do (3) and, if you check for when a subview has collapsed, you can make sure (2) doesn’t happen.

However, if you want to write less of your own code, there are other options. Even with a cursory search, I have found three NSSplitView replacements:

OASplitView from the Omni frameworks. Is a subclass of NSSplitView. Solves only (3).

KFSplitView from Ken Ferry. A subclass of NSSplitView. It solves (1) and (3). (2) still happens in its demo.

RBSplitView from Rainer Brockerhoff. Not a subclass of NSSplitView. Solves (1) and (3). Solves the first part of (2), but not the second.

Admittedly, the second part of (2) might not be possible in general-purpose framework code, since it can involve an invasive solution. I solved it in the code I was working on by subclassing the NSTableView located in the subview. In the subclass, I added an _acceptsFirstResponder flag, a setAcceptsFirstResponder: method to set the flag, and overrode acceptsFirstResponder to use the flag.

With these changes, it was easy to take a table out of the tab chain when the subview was collapsed, by merely sending it the set message. It would have been more onerous if I had to subclass multiple classes to get this same behavior in different types of widgets, in order to send the same set message in them all.

You may be able to get the same behavior out of some controls by sending them a setEnabled: message, but that wasn’t successful for me with tables.

Untied III: Furniture (Table) Polish

I used my last post to concentrate on the features I could implement with a data source-backed table, but not with a Cocoa bindings-backed table.

But the Category02.zip sample code from my last post has more to it than that.

For instance, in the data source-backed table, if you add a category – which starts with a blank name – and hit return without typing anything in, it deletes the category completely, under the assumption that you must not have wanted it if you weren’t going to give it a name.

However, if you erase the name of an existing category and hit return, it reverts it to the old name, just like the Finder does. This is a nice “cancel editing” shortcut, since Undo may not take you back all the way to the original name if you type enough.

One bit of polish I didn’t implement, but which I would want in a real app, is Escape key support. If you hit the Escape key or Command-period, it should abort editing.

I have a tendency to overdo my sample applications, so I think I’ll stop here with the category table features. Next up: the detail view of “thing” entries. (See the original post for the overall description of the Categorizer application.)

Untied II: Table Data Source Tricks

Last time around, I concentrated on what I could and couldn’t get a Cocoa bindings-based NSTableView to do for me.

Today, I’m going to do everything I mentioned last time, using the data source and delegate methods in listed in NSTableView.h.

As a starting parameter, I said that I wanted a special, unmodifiable, always-on-top Category entry in my table, called “All”. Making it uneditable is as easy as implementing the method tableView:shouldEditTableColumn:row: in my delegate class, and making it return NO if row == 0. I like this way better than making an “editable” Category accessor, because it puts the responsibility where it belongs, on the specific table that has the special entry, rather than putting a hack in the Category class to handle it.

Then, the issues. First up was sorting. I sort the contents of my Categories table the way I want, without having the pesky user being able to screw it up, by manually sorting the table’s contents in the data source method that’s called every time the user adds or changes a table entry, tableView:setObjectValue:forTableColumn:row:.

Because I never set the Sort Key in Interface Builder, as far as the table itself is concerned, the contents are completely unsorted. There’s no ascending or descending triangle in the column header.

Scott Anguish, in the comments of “Untied I”, suggested overriding NSArrayController to get the same behavior, and with another tweak (see that post’s comments) that does indeed get me exactly the same behavior. I implemented his suggestion in the Bindings part of the project that comes with today’s post. I complained in my reply that clicking on the column header still ends the editing session. Just a minor annoyance, to be sure, but the Mac is all about getting things exactly right. But it turns out my data source implementation has the same problem, so it’s a draw.

Second was editing, in three parts: starting an editing session immediately on add, resorting the entry immediately after the editing session ends, and beeping and not ending the editing session if the name is invalid.

To understand how I start the editing session, you need to know how I implement the action newCategory:. Before, this invoked the NSArrayController’s add method, which was asynchronous. Now, it does all the data model manipulation itself, and so it can go ahead and start the editing session without any worries afterward.

Resorting the array takes place, as stated above, in the data source method tableView:setObjectValue:forTableColumn:row:. The same code that does it when you first add a category also works when you rename a category.

For the last part, editing verification is available by implementing the NSControl delegate method control:textShouldEndEditing:. This method is not called if I’m using bindings.

The sample is called Categorizer02.zip.

Synchronization Whack-a-Mole: Deleting a Subscription from NetNewsWire Across Several Machines

Let’s say I’ve got NetNewsWire installed on two different machines, and I dutifully synchronize each machine’s copy of NetNewsWire on both opening and closing via NetNewsWire’s .Mac synchronization feature.

Let’s say I get sick of a particular RSS feed, such as, say, Ranchero.

I delete it from machine #1 and close NetNewsWire (and synchronize).

I open NetNewsWire on machine #2 (and synchronize). Ranchero is still there! I close NetNewsWire (and synchronize).

I open NetNewsWire again on machine #1 (and synchronize). Hey, Ranchero is back here now, too!

Rinse, repeat.

It’s probably happening because I chose the “Merge my list with the list on the server” option, instead of the “Make my list a copy of the list on the server” option.

But I shouldn’t have to choose the second option, and lose any changes from a second machine if I have two copies of NetNewsWire open at the same time – say, because I’m testing on two machines at once – in order to get the benefits of object deletion from .Mac synchronization. Deletions should propagate.

Otherwise, when I want to delete a subscription, I’m going to be playing whack-a-mole forever – or have to remember to delete the subscription manually from each machine at the same time. And isn’t that sort of manually synchronization exactly what .Mac synchronization was supposed to help you avoid?

P.S. In this case, the Ranchero feed’s survival isn’t so bad, since I was only using it as an example and want to keep it.

P.P.S. Yes, this is a lazyweb bug report, since Brent has shown the unnerving capability to hone in on my NetNewsWire posts without any further prompting. If I don’t hear from him in a couple of days, then I’ll email him.

Untied: When Bindings Aren’t Enough

I’ve found, when I try to use Cocoa bindings for my user interface, that it’s never quite flexible enough for what I need.

Instead of trying to list the hurdles I’ve encountered in the abstract, I’m going to walk you through the first steps of a simplified example, called Categorizer, which is also available for download. This way, you can see the things I’ve figured out how to do, in addition to the dealbreakers.

Categorizer initial table

The Categorizer’s data model consists of Things and Categories.

The UI pictured above is the master table, of Categories. The detail table, of Things in the selected Categories, will be to the left. We won’t get to the detail table today.

The first category, "All," is special: you can’t delete it or rename it. It will contain all the things, and is always listed at the top of the category table, which is otherwise alphabetically sorted. You "categorize" things by dragging them onto the rows you’ve made yourself in the category table.

Sorting
The first issue is sorting. As I said above, categories need to be sorted alphabetically except for the "All," which is always on top.

NSArrayController will automatically sort its array – the contents of the table – according to the Sort Key and Sort Selector of the columns in the table, which are set in Interface Builder. The Sort Key needs to be set to something, it can’t be left blank if I want sorting. So I can’t, for example, try to sort on the entire Category class; I need to pick an attribute. Normally I’d pick something like "name," but in my case I want to into account the special "All" status and name. So I make a "specialSortKey" Category method and specify that in IB.

The table doesn’t start out sorted. I fix this not by manipulating the table, but by manipulating the NSArrayController. I get the sortDescriptorPrototype from the column, stick it in an NSArray, and use it as the parameter for a setSortDescriptors: message to the controller. (See the download for the exact code.) This also selects the column header and sets the indicator image (the little up or down arrow).

Turns out, though, that I don’t want this to act like a normal table column. I want the contents to be sorted the same way all the time, so I don’t want the up or down arrow in the column header that the user can toggle between. Because this would put "All" at the bottom, for one. But because of the array/controller integration, it’s basically not possible to have one without the other if I want to use an NSArrayController. This, it turns out, is dealbreaker #1.

Editing
I also want specific behavior when adding a new category. The standard NSArrayController behavior, if I connect the "Add" button to the controller’s add: action, is that a new, empty instance is added to the array and to the table. What I want, in addition, is that you automatically start editing that instance.

My first thought is to connect the "Add" button to a method in my own delegate, newCategory:. This method invokes the controller’s add: method and then starts the editing session.

Unfortunately, the new row is not yet made by the time add: returns. I’m not sure why this would be an asynchronous process, but it is. Oh, wait, here we go, from the documentation: "Note: In Mac OS X v10.4 the target-action methods are deferred so that the error mechanism can provide feedback as a sheet."

I then try implementing MyDelegate’s key-value coding method insertObject:inCategoriesAtIndex: so that, after it does its business, it initiates editing of the added row. But for some reason (maybe because it’s a horrible hack) that doesn’t work either.

This, so far, is dealbreaker #2.

After I’m done editing, I want the renamed category to be re-sorted properly. This is pretty easy, but still involves a gotcha. On 10.4, the controlTextDidEndEditing: NSTableView delegate method is never invoked when you double-click a table cell and edit it in a table that uses an NSArrayController. In my tests, it seems to still work in a table that doesn’t use an NSArryController.

Luckily, there’s another Cocoa API in a superclass of NSTableView, NSControl’s controlTextDidEndEditing:. If I define that in my table delegate, I can use it to send the rearrangeObjects message to my controller, and the renamed category will be immediately resorted.

My last editing issue, and last issue for this post, since it’s getting long, is uniqueness. I don’t want you to be able to enter the same category name twice. If you try to end editing with an invalid name, I want the UI to beep and continue the editing session.

I try to do this in the key-value coding way by implementing the "name"-specific validation method validateName:error: in Category. There’s some finagling, since now Category needs to be able to access the full list of categories in MyDelegate. But, with that achieved, Category’s validateName:error: now returns NO if the new name is the same as any other name.

Can I make it beep? Nope. My only choice of error feedback for the user is an error dialog. There are plenty of options in NSError so I can customize what the dialog says, but no option to do something else.

Dealbreaker #3.

The download, called Categorizer01.zip, contains the work I’ve described so far, including the behavior I couldn’t get working right. It’s an Xcode 2.1 project, sorry, it can’t be opened in Xcode 2.0.

If my schedule permits, soon I will make a second Categorizer, one that doesn’t use Cocoa bindings, that customizes the behavior as I wanted.