-
Notifications
You must be signed in to change notification settings - Fork 8
/
WebmailerDaemon.m
594 lines (501 loc) · 18.7 KB
/
WebmailerDaemon.m
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
#import "WebmailerDaemon.h"
#import "DefaultsDomain.h"
#import "ImageForStateTransformer.h"
#import "NSString+PrototypeExpansion.h"
#import "WebmailerConfiguration.h"
#import "WebmailerShared.h"
#import "ScriptingBridgeApps.h"
// to stop warnings on pre-10.6 builds
@interface NSEvent (ComBelkadanWebmailer_NoWarn)
+ (NSUInteger)modifierFlags;
@end
@interface WebmailerDaemon ()
@property(readwrite,copy) NSString *mailtoURL;
@property(readwrite,copy) NSURL *sourceAppURL;
@property(readwrite,assign) ComBelkadanWebmailer_Configuration *activeConfiguration;
- (void)openURLString:(NSString *)mailtoURLString fromApplicationAtURL:(NSURL *)appURL;
- (void)launchShellScript:(NSString *)script;
- (void)launchTerminal:(NSString *)script;
- (NSURL *)chooseAppForOpeningURL:(NSURL *)url;
@end
/*!
* Returns YES if the shift key is currently held, NO if not.
*/
BOOL isShiftKeyDown ()
{
BOOL result;
#if defined(MAC_OS_X_VERSION_10_6) && MAC_OS_X_VERSION_10_6 <= MAC_OS_X_VERSION_MAX_ALLOWED
# if MAC_OS_X_VERSION_10_6 <= MAC_OS_X_VERSION_MIN_REQUIRED
result = (([NSEvent modifierFlags] & NSShiftKeyMask) != 0);
# else /* MAC_OS_X_VERSION_MIN_REQUIRED < MAC_OS_X_VERSION_10_6 */
if ([NSEvent respondsToSelector:@selector(modifierFlags)])
{
result = (([NSEvent modifierFlags] & NSShiftKeyMask) != 0);
}
else
{
CGEventRef tempEvent = CGEventCreate(NULL /*default event source*/);
result = ((CGEventGetFlags(tempEvent) & kCGEventFlagMaskShift) != 0);
CFRelease(tempEvent);
}
# endif
#else /* MAC_OS_X_VERSION_MAX_ALLOWED < MAC_OS_X_VERSION_10_6 */
CGEventRef tempEvent = CGEventCreate(NULL /*default event source*/);
result = ((CGEventGetFlags(tempEvent) & kCGEventFlagMaskShift) != 0);
CFRelease(tempEvent);
#endif
return result;
}
/*!
* Returns the image used to represent the currently selected destination.
*/
NSImage *GetActiveDestinationImage ()
{
NSImage *image;
// If available, use the active state image that comes with Mac OS X.
#if defined(MAC_OS_X_VERSION_10_6) && MAC_OS_X_VERSION_10_6 <= MAC_OS_X_VERSION_MAX_ALLOWED
if (&NSImageNameStatusAvailable != NULL)
{
image = [NSImage imageNamed:NSImageNameStatusAvailable];
}
else
#endif
{
// Otherwise, use our own bundled image.
image = [NSImage imageNamed:@"active"];
}
return image;
}
/*!
* Finds the URL of a running application with a given process serial number.
* If the URL cannot be found, for whatever reason (perhaps the application has
* quit), returns nil.
*/
NSURL *GetURLForPSN (const ProcessSerialNumber *psn) {
if (psn->highLongOfPSN == 0 && psn->lowLongOfPSN == 0)
return nil;
FSRef bundleLocation;
OSStatus status = GetProcessBundleLocation(psn, &bundleLocation);
if (status != noErr)
return nil;
NSURL *appURL = (NSURL *)CFURLCreateFromFSRef(NULL, &bundleLocation);
return [appURL autorelease];
}
/*!
* Returns YES if the application at the first URL can open the second URL.
* Basically a wrapper around LSCanURLAcceptURL().
*/
BOOL CanOpenURLWithApplication (NSURL *url, NSURL *appURL) {
Boolean canHandle;
OSStatus status = LSCanURLAcceptURL((CFURLRef)url, (CFURLRef)appURL, kLSRolesViewer, kLSAcceptDefault, &canHandle);
if (status != noErr)
return NO;
return canHandle ? YES : NO; // conversion from Boolean to BOOL...doesn't hurt!
}
/*!
* Open a URL using the given application.
* Essentially a wrapper around LSOpenFromURLSpec().
*/
void OpenURLWithApplication(NSURL *url, NSURL *appURL)
{
LSLaunchURLSpec launchSpec = {
.appURL = (CFURLRef)appURL,
.itemURLs = (CFArrayRef)[NSArray arrayWithObject:url],
.launchFlags = kLSLaunchDefaults
};
(void)LSOpenFromURLSpec(&launchSpec, NULL);
}
/*!
* Returns the URL of the application the Finder would use to open this URL.
* If there is no application registered for the URL, returns nil.
* Basically a wrapper around LSGetApplicationForURL().
*/
NSURL *GetDefaultAppURLForURL(NSURL *url) {
// TODO: in 10.6, replace this function with -[NSWorkspace URLForApplicationToOpenURL:]
NSURL *appURL;
OSStatus status = LSGetApplicationForURL((CFURLRef)url, kLSRolesAll, NULL, (CFURLRef*)&appURL);
if (status != noErr) return nil;
return [appURL autorelease];
}
NSURL *GetURLForApplicationWithBundleIdentifier (NSString *bundleID)
{
// TODO: in 10.6, replace this function with -[NSWorkspace URLForApplicationWithBundleIdentifier:]
NSURL *appURL;
OSStatus status = LSFindApplicationForInfo(kLSUnknownCreator, (CFStringRef)bundleID, NULL, NULL, (CFURLRef *)&appURL);
if (status != noErr) return nil;
return [appURL autorelease];
}
#pragma mark -
/*!
* The central controller for the faceless Webmailer application, tucked away inside
* the preference pane bundle. WebmailerDaemon's entire purpose is to launch, respond
* to a single Apple Event (GetURL, with a mailto: URL), and quit. If the application
* is launched before the preference pane has ever been opened (an unlikely event),
* the preference pane is opened so that the user can configure the settings.
* <br/>
* However, should the shift key be held down when Webmailer launches, the user
* gets to choose which destination URL to use, via a small window that pops up
* in the center of the screen. After this choice, the evaluation of the URL
* continues as before.
* <br/>
* Why does Webmailer quit, instead of just remaining open? Well, if it didn't,
* the user might have some trouble if they wanted to uninstall it (hopefully that
* wouldn't happen anyway). They'd have to log out and log back in first, because
* there's no way to actually quit it (well, besides Activity Monitor or kill).
* As far as I can tell there's no significant time lag. (This does make the name
* "daemon" less applicable, but the behavior of a faceless app still applies).
*/
@implementation WebmailerDaemon
@synthesize mailtoURL, sourceAppURL;
- (id)init
{
self = [super init];
if (!self) return nil;
// Make sure the user has configured Webmailer before.
defaults = [[DefaultsDomain domainForName:WebmailerAppDomain] retain];
if ([defaults count] == 0)
{
[self openPreferencePane:nil];
}
// Quit if no incoming event.
[NSTimer scheduledTimerWithTimeInterval:10 target:self selector:@selector(quitIfInactive:) userInfo:nil repeats:YES];
return self;
}
- (void)dealloc
{
[defaults release];
[mailtoURL release];
[sourceAppURL release];
[configurations release];
[super dealloc];
}
- (void)quitIfInactive:(NSTimer *)timer
{
if (!mailtoURL)
[NSApp terminate:nil];
}
/*!
* Set up the configuration chooser, then display it and bring the Webmailer app to the front.
* The user can then choose which configuration to use for the URL that was clicked.
*/
- (void)showConfigurationChooser {
// Create a new value transformer that shows an active icon for true values.
ImageForStateTransformer *transformer = [[ImageForStateTransformer alloc] initWithTrueImage:GetActiveDestinationImage() falseImage:nil];
[NSValueTransformer setValueTransformer:transformer forName:@"ImageForState"];
[transformer release];
// Select the active destination.
NSArray *configs = self.configurations;
NSUInteger count = [configs count];
NSUInteger i;
for (i = 0; i < count; i += 1)
{
if ([[configs objectAtIndex:i] isActive])
{
[configurationController setSelectionIndex:i];
[configurationTable scrollRowToVisible:i];
break;
}
}
// Set the table to fire an action on double-click.
[configurationTable setDoubleAction:@selector(confirmConfiguration:)];
[configurationTable setTarget:self];
// Bring up the configuration window.
[NSApp activateIgnoringOtherApps:YES]; // needed because we're an NSUIElement
[configurationWindow makeKeyAndOrderFront:self];
}
/*!
* Opens the Webmailer preference pane, using a file open if Webmailer is in the pane's Resources,
* and either Scripting Bridge or NSAppleScript if it is outside.
*/
- (IBAction)openPreferencePane:(id)sender
{
NSString *prefPanePath = [[[[[NSBundle mainBundle] bundlePath] stringByDeletingLastPathComponent] stringByDeletingLastPathComponent] stringByDeletingLastPathComponent];
if ([prefPanePath hasSuffix:@".prefPane"])
{
[[NSWorkspace sharedWorkspace] openFile:prefPanePath];
}
else
{
SystemPreferencesApplication *sysPrefs = [SBApplication applicationWithBundleIdentifier:@"com.apple.systempreferences"];
[sysPrefs setDelegate:self];
[sysPrefs activate];
[sysPrefs setCurrentPane:[[sysPrefs panes] objectWithID:WebmailerAppDomain]];
}
[NSApp terminate:self];
}
/*!
* Confirms the user's choice of destination configuration, and launches the
* destination URL.
*/
- (IBAction)confirmConfiguration:(id)sender
{
[configurationWindow orderOut:sender];
[self launchDestination:[[configurationController selection] valueForKey:WebmailerDestinationURLKey]];
}
#pragma mark -
- (NSArray *)configurations
{
if (!configurations) {
if (![NSThread isMainThread]) {
[self performSelectorOnMainThread:_cmd withObject:nil waitUntilDone:YES];
} else {
// Set up sort descriptors for destination URLs.
NSSortDescriptor *sortByName = [[NSSortDescriptor alloc] initWithKey:WebmailerDestinationNameKey ascending:YES selector:@selector(localizedCaseInsensitiveCompare:)];
NSSortDescriptor *sortByDestination = [[NSSortDescriptor alloc] initWithKey:WebmailerDestinationURLKey ascending:YES];
NSArray *sortDescriptors = [[NSArray alloc] initWithObjects:sortByName, sortByDestination, nil];
[sortByName release];
[sortByDestination release];
// Sort the configurations.
NSArray *sortedDictionaries = [[defaults objectForKey:WebmailerConfigurationsKey] sortedArrayUsingDescriptors:sortDescriptors];
[sortDescriptors release];
NSMutableArray *allConfigurations = [[NSMutableArray alloc] initWithCapacity:[sortedDictionaries count]];
for (NSDictionary *dict in sortedDictionaries) {
id next = [[ComBelkadanWebmailer_Configuration alloc] initWithDictionaryRepresentation:dict];
[next addObserver:self forKeyPath:WebmailerDestinationNameKey options:0 context:(void *)[WebmailerDaemon class]];
[next addObserver:self forKeyPath:WebmailerDestinationURLKey options:0 context:(void *)[WebmailerDaemon class]];
[allConfigurations addObject:next];
[next release];
}
configurations = allConfigurations;
}
}
return configurations;
}
- (ComBelkadanWebmailer_Configuration *)activeConfiguration
{
if (!activeConfiguration) {
if (![NSThread isMainThread]) {
[self performSelectorOnMainThread:_cmd withObject:nil waitUntilDone:YES];
} else {
for (ComBelkadanWebmailer_Configuration *next in self.configurations) {
if (next.active) {
activeConfiguration = next;
break;
}
}
NSAssert(activeConfiguration != nil, @"No active configuration found.");
}
}
return activeConfiguration;
}
- (void)setActiveConfiguration:(ComBelkadanWebmailer_Configuration *)newActive
{
activeConfiguration.active = NO;
newActive.active = YES;
activeConfiguration = newActive;
[defaults beginTransaction];
[defaults setObject:[configurations valueForKey:@"dictionaryRepresentation"] forKey:WebmailerConfigurationsKey];
[defaults setObject:newActive.destinationURL forKey:WebmailerCurrentDestinationKey];
[defaults endTransaction];
}
- (BOOL)application:(NSApplication *)sender delegateHandlesKey:(NSString *)key
{
return [key isEqual:@"configurations"] || [key isEqual:@"activeConfiguration"];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
if (context == (void *)[WebmailerDaemon class]) {
[defaults beginTransaction];
ComBelkadanWebmailer_Configuration *changedConfiguration = object;
if (changedConfiguration.active && [keyPath isEqual:WebmailerDestinationURLKey]) {
[defaults setObject:changedConfiguration.destinationURL forKey:WebmailerCurrentDestinationKey];
}
[defaults setObject:[configurations valueForKey:@"dictionaryRepresentation"] forKey:WebmailerConfigurationsKey];
[defaults endTransaction];
} else {
[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
}
}
#pragma mark -
- (void)openURLString:(NSString *)mailtoURLString fromApplicationAtURL:(NSURL *)appURL;
{
self.mailtoURL = mailtoURLString;
self.sourceAppURL = appURL;
[defaults refresh];
BOOL shouldShowConfigurations = isShiftKeyDown();
if (shouldShowConfigurations)
{
[self showConfigurationChooser];
}
else
{
[self launchDestination:[defaults objectForKey:WebmailerCurrentDestinationKey]];
}
}
- (void)launchDestination:(NSString *)destinationPrototype
{
if ([destinationPrototype characterAtIndex:0] == '#')
{
// It's a background shell script!
destinationPrototype = [destinationPrototype substringFromIndex:1];
NSString *script = [destinationPrototype replaceWebmailerPlaceholdersUsingMailtoURLString:mailtoURL alwaysEscapeQuotes:YES];
[self launchShellScript:script];
}
else
{
NSString *destination = [destinationPrototype replaceWebmailerPlaceholdersUsingMailtoURLString:mailtoURL alwaysEscapeQuotes:NO];
NSURL *destinationURL = [[NSURL alloc] initWithString:destination];
if (destinationURL)
{
// It's a valid URL
NSURL *appURL = [self chooseAppForOpeningURL:destinationURL];
if (appURL)
{
OpenURLWithApplication(destinationURL, appURL);
}
else
{
// Fall back to default URL handler.
[[NSWorkspace sharedWorkspace] openURL:destinationURL];
}
[destinationURL release];
}
else
{
// It's not a valid URL; must be an "open Terminal" shell script.
NSString *script = [destinationPrototype replaceWebmailerPlaceholdersUsingMailtoURLString:mailtoURL alwaysEscapeQuotes:YES];
[self launchTerminal:script];
}
}
self.mailtoURL = nil;
}
/*!
* Returns the URL of the application Webmailer should use to open the given
* destination URL. If smart app choosing is disabled in the preferences, or
* if no good applications can be found, returns nil.
*
* 1. If the user has explicitly chosen an app, pick that (if it can handle the destination).
* 2. If the app that sent us the mailto URL can handle the destination, pick that.
* 3. If the default app for the destination URL is open, pick that.
* 4. If any app that can handle the destination URL is open, pick one of them.
* 5. Otherwise, give up and return nil.
*/
- (NSURL *)chooseAppForOpeningURL:(NSURL *)url
{
NSNumber *choosingMode = [defaults objectForKey:WebmailerBrowserChoosingModeKey];
if (!choosingMode) choosingMode = [defaults objectForKey:WebmailerDisableAppChoosingKey];
BrowserChoice mode = [choosingMode unsignedIntegerValue];
switch (mode) {
case BrowserChoiceSystemDefault:
return nil;
case BrowserChoiceSpecific: {
NSString *identifier = [defaults objectForKey:WebmailerChosenBrowserIDKey];
if (identifier)
{
NSURL *chosenAppURL = GetURLForApplicationWithBundleIdentifier(identifier);
if (chosenAppURL && CanOpenURLWithApplication(url, chosenAppURL)) return chosenAppURL;
}
break;
case BrowserChoiceBestGuess:
break;
}
}
// If the app that sent us the mailto URL can handle the destination, pick that.
if (sourceAppURL && CanOpenURLWithApplication(url, sourceAppURL)) return sourceAppURL;
// Otherwise, start scanning the list of open applications.
NSURL *defaultAppURL = GetDefaultAppURLForURL(url);
NSWorkspace *workspace = [NSWorkspace sharedWorkspace];
NSURL *currentChoice = nil;
#if defined(MAC_OS_X_VERSION_10_6) && MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_6
if ([workspace respondsToSelector:@selector(runningApplications)])
{
for (NSRunningApplication *nextApp in [workspace runningApplications])
{
NSURL *nextAppURL = nextApp.bundleURL;
if ([nextAppURL isEqual:defaultAppURL])
{
return defaultAppURL;
}
else if (!currentChoice && CanOpenURLWithApplication(url, nextAppURL))
{
currentChoice = nextAppURL;
}
}
}
else
#endif
{
for (NSDictionary *nextAppInfo in [workspace launchedApplications])
{
NSURL *nextAppURL = [NSURL fileURLWithPath:[nextAppInfo objectForKey:@"NSApplicationPath"]];
if ([nextAppURL isEqual:defaultAppURL])
{
return defaultAppURL;
}
else if (!currentChoice && CanOpenURLWithApplication(url, nextAppURL))
{
currentChoice = nextAppURL;
}
}
}
return currentChoice;
}
/*!
* Run the given script in the user's preferred shell. The output goes to stdout.
*/
- (void)launchShellScript:(NSString *)script
{
// Find out the user's preferred shell. Default to /bin/sh.
NSString *userShell = [[[NSProcessInfo processInfo] environment] objectForKey:@"SHELL"];
if ([userShell length] == 0) userShell = @"/bin/sh";
// Launch the shell.
NSTask *task = [[NSTask alloc] init];
NSPipe *pipe = [NSPipe pipe];
[task setStandardInput:[pipe fileHandleForReading]];
[task setLaunchPath:userShell];
[task launch];
// Write the script.
NSFileHandle *input = [pipe fileHandleForWriting];
[input writeData:[script dataUsingEncoding:NSUTF8StringEncoding]];
[input writeData:[NSData dataWithBytesNoCopy:"\n" length:1 freeWhenDone:NO]];
[input closeFile];
[task waitUntilExit];
[task release];
}
/*!
* Run the given script in the Terminal, so it can be used interactively.
*
* TODO: use handler for com.apple.Terminal.shell-script ?
*/
- (void)launchTerminal:(NSString *)script
{
TerminalApplication *terminal = [SBApplication applicationWithBundleIdentifier:@"com.apple.Terminal"];
[terminal setDelegate:self];
[terminal activate];
(void)[terminal doScript:script in:nil];
}
#pragma mark -
// For SBApplicationDelegate
- (id)eventDidFail:(const AppleEvent *)event withError:(NSError *)error
{
return nil; // be stupid but forgiving
}
@end
@implementation NSApplication (ComBelkadanWebmailer)
/*!
* Handles an 'open URL' AppleScript event. Application-targeted commands can't be
* passed off for free to delegates like properties can, so this method on NSApplication
* handles the nasty bits of Cocoa Scripting and passes clean URLs to the main controller.
*
* See -[WebmailerDaemon openURLString:fromApplicationAtURL:]
*/
- (void)ComBelkadanWebmailer_openURL:(NSScriptCommand *)command {
NSAppleEventDescriptor *event = [command appleEvent];
NSString *directObject = [command directParameter];
// Check that we really have a mailto URL.
NSURL *mailtoURLObject = [NSURL URLWithString:directObject];
if (!mailtoURLObject || ![WebmailerMailtoScheme isEqual:[mailtoURLObject scheme]]) {
[command setScriptErrorNumber:paramErr];
[command setScriptErrorString:@"not a valid mailto URL"];
NSAppleEventDescriptor *directObjectDesc = [event paramDescriptorForKeyword:keyDirectObject];
[command setScriptErrorOffendingObjectDescriptor:directObjectDesc];
return;
}
ProcessSerialNumber psn;
NSData *data = [[[event attributeDescriptorForKeyword:keyAddressAttr] coerceToDescriptorType:typeProcessSerialNumber] data];
NSAssert([data length] <= sizeof(psn), @"PSN key is too big!");
[data getBytes:&psn];
WebmailerDaemon *delegate = (WebmailerDaemon *)[self delegate];
[delegate openURLString:directObject fromApplicationAtURL:GetURLForPSN(&psn)];
}
@end