Using NSAppleScript with Sierra - send action not happening - macos

I am using NSAppleScript for sending email from my OSX application.
It was working fine before but now when I use new XCode 9.0 with Sierra 10.12.6 I have one problem. Application will create email and open Mail window containing email and message content but it wont perform SEND. I can click on send button and everything is fine, mail will send but I would like to avoid manual click-on-send-button action.
I am using entitlements:
<plist version="1.0">
<dict>
<key>com.apple.security.scripting-targets</key>
<dict>
<key>com.apple.mail</key>
<array>
<string>com.apple.mail.compose</string>
</array>
</dict>
<key>com.apple.security.app-sandbox</key>
<true/>
<key>com.apple.security.network.client</key>
<true/>
</dict>
</plist>
And this is the code:
NSString *emailString = [NSString stringWithFormat:#"\
tell application \"Mail\"\n\
set newMessage to make new outgoing message with properties {subject:\"%#\", content:\"%#\" & return} \n\
tell newMessage\n\
set visible to false\n\
set sender to \"%#\"\n\
make new to recipient at end of to recipients with properties {name:\"%#\", address:\"%#\"}\n\
tell content\n\
",subjectt, bodyText, #"Company", toAddress, toAddress];
emailString = [emailString stringByAppendingFormat:#"\
end tell\n\
set visible to false\n\
send newMessage\n\
end tell\n\
end tell"];
NSLog(#"%#",emailString);
NSAppleScript *emailScript = [[NSAppleScript alloc] initWithSource:emailString];
[emailScript executeAndReturnError:nil];
So everything is in place but send action is failing.
Again it was working well with older OSX version(s)..
Am I missing entitlement or something?
Thank you!

It looks like you tell the newMessage to send the newMessage (double reference)
Delete newMessage in the send line

Long story short. This is the wrong way. With modern OSX use NSSharingService.
NSArray * shareItems = [NSArray arrayWithObjects:bodyText, nil];
NSArray * recepiants = [NSArray arrayWithObjects:toAddress, nil];
NSSharingService *service = [NSSharingService sharingServiceNamed:NSSharingServiceNameComposeEmail];
[service setRecipients:recepiants];
[service setSubject:subjectt];
service.delegate = self;
[service performWithItems:shareItems];
When it comes down to sending email with some sort of results from application NSAppleScript is not your friend anymore. OSX is becoming more like iOS nowadays.

Related

Not able to connect to XPCService in a Launch agent

I have created an XPC Service .
the server side code briefly is
NSXPCListener *listener = [[NSXPCListener alloc] initWithMachServiceName:#"test.xpcserver"];
listener.delegate = delegate;
[listener resume];
this is installed as a Launch Agent using info.plist as
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>TestServer</string>
<key>MachServices</key>
<dict>
<key>test.xpcserver</key>
<true/>
</dict>
<key>ProgramArguments</key>
<array>
<string>/Applications/test.app/Contents/XPCServices/xpcserver.xpc/Contents/MacOS/xpcserver</string>
</array>
<key>RunAtLoad</key>
<true/>
</dict>
</plist>
The server is running fine and i can see it running with launchctl list
The client side code is in another app and the code for connect is:
connection = [[NSXPCConnection alloc] initWithMachServiceName:#“test.xpcserver” options:0];
connection.remoteObjectInterface = [NSXPCInterface interfaceWithProtocol:#protocol(xpcserverprotocol)];
[connection resume];
service = [connection remoteObjectProxyWithErrorHandler:^(NSError * _Nonnull error) { }];
[service ServerFunc:#“howdy” withReply:^(NSString *result) {
NSLog(#"%#",result);
}];
but not able to connect to server .
any pointers as to what going wrong ?
It's hard to tell from what you've posted. Things to check:
ensure your listener delegate is implementing right function signature for shouldAcceptNewConnection(). If it's not right, you will not get any error. It should be
- (BOOL)listener:(NSXPCListener *)listener shouldAcceptNewConnection:(NSXPCConnection *)conn { your code here }
Use System Logging (NSLog) or add logging of stdout and stderr for your service. See the launch.sh in this hello_world_xpc example, which generates the plist based on local paths. That example is using the C API, so not exactly what you are looking for.
See old Objective-C hello.tar.gz example here : afewguyscoding.com/2012/07/ipc-easy-introducing-xpc-nsxpcconnection/
(Posting this for future readers, I imagine OP solved their problem by now :) )
Some other things to check:
Make sure your launch agent/deamon is using the -[NSXPCListener initWithMachServiceName], and not +[NSXPCListener serviceListener].
The latter is only useful for regular XPC service (which would be stored in the App's bundle, not in /Library/.... If you mix it up, you'll get a crash report in Console.app that will say:
Configuration error: Couldn't retrieve XPCService dictionary from service bundle.
In your agent/daemon, don't forget to start the main run loop after [listener resume]; with [[NSRunLoop currentRunLoop] run];
In your agent/daemon, make sure there's a strong reference to keep your listener's delegate alive. The -[NSXPCListener delegate] property is declared weak, so it's possible that your listener deallocates immediately after you set it. I would put an NSLog statement in the deinit of your delegate's class, just to be sure.

How to register service from app in macOS application?

I am trying to implement a contextual menu item that would be presented in services when a text is selected. So e.g., if I select a word in a TextEdit, I want a menu item "Do my stuff" to be presented in contextual menu, which would feed the selected word to my application code for further processing.
By googling a little research, I came to conclusion that I need to implement and register a service. I tried to follow
Providing Services
documentation, but that seems at least a bit outdated (not to mention in some places vague).
From what I understood, I was able to do the following:
I implemented service object:
import Foundation
import AppKit
class ContextualMenuServiceProvider {
func importString(_ pasteBoard: Pasteboard, userData: String, error: String) {
print(">>>> in import string from service consumer")
}
}
I created an entry in Info.plist for service:
<key>NSServices</key>
<array>
<dict>
<key>NSMenuItem</key>
<dict>
<key>default</key>
<string>Import to $(PRODUCT_NAME)</string>
</dict>
<key>NSMessage</key>
<string>importString</string>
<key>NSPortName</key>
<string>MyApp</string>
<key>NSSendTypes</key>
<array>
<string>public.plain-text</string>
</array>
</dict>
</array>
And finally, I tried to register the service in AppDelegate. Now the documentation says to use:
NSApp.setServicesProvider(encryptor)
// where encryptor is an object of my ContextualMenuServiceProvider
However, it seems that NSApp does not have setServicesProvider method. I tried to use:
NSApp.servicesProvider = ContextualMenuServiceProvider()
property, however, this does not seem to work either.
I test it by running the application from the Xcode, and then, while the application is running, I try to select some text in TextEdit (it should not be restricted to TextEdit, I just use it as an example), and after right click I am looking for my menu item. It does not work.
I was able to find some kind of similar SO questions (e.g., Creating an os x service, or Howto add menu item to Mac OS Finder in Delphi XE2), but I was not able to detect what I am doing wrong from them (not to mention that most of them are old (using setServiceProvider), or using languages like C# or Delphi).
Any idea what is wrong with my code/approach?
Ok, looks like I have made several small mistakes that lead me to believe it was not working. However, in the end I was able to make it work. So if you are facing the same task, here is a solution in Swift 3:
(1) You have to implement a service method:
import Cocoa
class ServiceProvider: NSObject {
let errorMessage = NSString(string: "Could not find the text for parsing.")
#objc func service(_ pasteboard: NSPasteboard, userData: String?, error: AutoreleasingUnsafeMutablePointer<NSString>) {
guard let str = pasteboard.string(forType: NSStringPboardType) else {
error.pointee = errorMessage
return
}
let alert = NSAlert()
alert.messageText = "Hello \(str)"
alert.informativeText = "Welcome in the service"
alert.addButton(withTitle: "OK")
alert.runModal()
}
}
Here important thing that I did not know before was that the object providing the service has to subclass NSObject (it can also be ViewController, or any other NSObject subclass). Services API uses selector to call it, and selector technology works with NSObject only.
Also, service method has to have this declaration: it takes 3 arguments, first is NSPasteboard (you can use Pasteboard, but that does not provide string method) that is not labeled, second is optional String labeled userData, third is AutoreleasingUnsafeMutablePointer<NSString> labeled error. Follow this to get best type safety while still making it work. Otherwise the services API won't be able to find it and call it. You can use UnsafeRawPointers for all its arguments, but you will not gain anything by that.
(2) In the app delegate (or documentation says that you can do it anywhere, but I am doing it here) you register the service provider:
import Cocoa
#NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate {
#IBOutlet weak var window: NSWindow!
func applicationDidFinishLaunching(_ aNotification: Notification) {
NSApplication.shared().servicesProvider = ServiceProvider()
NSUpdateDynamicServices()
}
}
It seems to be working also without NSUpdateDynamicServices call, but I guess better be safe than sorry - it should refresh the services in the system so that you do not have to logoff and login the user to get the current service version.
(3) Finally, you have to configure the Info.plist to advertise your service method:
<key>NSServices</key>
<array>
<dict>
<key>NSMessage</key>
<string>service</string>
<key>NSPortName</key>
<string>serviceTest</string>
<key>NSMenuItem</key>
<dict>
<key>default</key>
<string>Test hello world</string>
</dict>
<key>NSRestricted</key>
<false/>
<key>NSRequiredContext</key>
<dict/>
<key>NSSendTypes</key>
<array>
<string>NSStringPboardType</string>
</array>
</dict>
</array>
This is an XML source of the Info.plist - although Apple recommends to use their editor, in Xcode 8.2 I was not able to add an NSRequiredContext key through the editor - I had to open the file as Source code and add it manually. I would recommend editing directly the XML source.
You can find the documentation about the meaning of each key in Services Properties, but I would like to mention some crucial points. First, you NEED the NSRequiredContext key - they mention it in the documentation, but the editor does not support it - edit XML directly and add it even empty (as I do). Second, if you are using NSSendTypes or NSReturnTypes and you want to work with strings, use NSStringPboardType even though its documentation says it's deprecated and should be substituted by NSPasteboardTypeString - the latter won't work. Finally, NSMessage key with service value is the name of the service method. So my service provider object declares following method:
#objc func service(_ pasteboard: NSPasteboard, userData: String?, error: AutoreleasingUnsafeMutablePointer<NSString>)
I am using the service value in NSMessage. If you want to change it (e.g., you want several service methods) just don't forget that these two has to match (value in Info.plist configuration and the name of the service method).
(4) In this state it should be working. There is one thing I would like to mention that might help you with debugging. Test it using the method mentioned in documentation at the end by running:
/Applications/TextEdit.app/Contents/MacOS/TextEdit -NSDebugServices com.mycompany.myapp
Running this from Terminal should report whether any service using provided bundle is registered, and also whether it will be presented - e.g., in my case the problem was that I had not provided the mandatory NSRequiredContext. After testing it using this approach I was able to recognise that the service was installed, and the problem was that the services API assumed it should be filtered out. After some experiments and googling I solved it by adding the empty NSRequiredContext.
After each change to the service I recommend to quit TextEdit and run it again, it seems that it keeps a reference to the old service provider object (or something like that, changes were not registered by TextEdit if I did not restart it).
The minimum requirements for text services in info.plist. All items can be added through Xcode (tested on version 14.2):
<key>NSServices</key>
<array>
<dict>
<key>NSMessage</key>
<string>message</string>
<key>NSMenuItem</key>
<dict>
<key>default</key>
<string>Title</string>
</dict>
<key>NSRequiredContext</key>
<dict/>
<key>NSSendTypes</key>
<array>
<string>NSStringPboardType</string>
</array>
</dict>
</array>
In AppDelegate:
class AppDelegate: NSObject, NSApplicationDelegate {
func applicationDidFinishLaunching(_ notification: Notification) {
NSApp.servicesProvider = self
}
#objc func message(_ pasteboard: NSPasteboard, userData: String?, error: NSErrorPointer) {
guard let content = pasteboard.string(forType: .string) else { return }
// Use `content`…
}
}
The system won’t update services every time after app’s launching. Remove the app entirely, then build and run again so the services can be refreshed. Run pbs to check if the service is correctly configured under your bundle identifier:
/System/Library/CoreServices/pbs -dump_pboard

How to register for “Apps using location” on the Mac

I’m playing with Wi-Fi Positioning system (WPS) on the Mac.
Attempting to specify the most exact positioning possible. I’ve noticed that apps such as Maps register in the Location notification of the OS
Yet my code does not seem to generate this effect,
- (void)startStandardUpdates
{
// Create the location manager if this object does not
// already have one.
if (nil == locationManager)
locationManager = [[CLLocationManager alloc] init];
locationManager.delegate = self;
locationManager.desiredAccuracy = kCLLocationAccuracyBestForNavigation;
// Set a movement threshold for new events.
locationManager.distanceFilter = 1; // meters
[locationManager startUpdatingLocation];
}
Am I missing something?
Is your app sandboxed?
If it is, you need to enable the "App Data" -> "Location" entitlement in the "Capabilities" tab of your target.
Otherwise the CLLocationManager silently fails to start.
Another interesting point is, that your app only shows up in this list when the CLLocationManager is actually running. Calling stopUpdatingLocation deregisters your app from the location agent.
A minimal entitlement file for a OS X app using Core Location looks like:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.app-sandbox</key>
<true/>
<key>com.apple.security.personal-information.location</key>
<true/>
</dict>
</plist>

App rejected due to temporary entitlement I need for NSSavePanel

this is the second time my Mac app has been rejected by the MAS.
I use a temporary entitlement so that the user can store his backup file the app created on his device.
This is the response I am getting from the MAS to my rejection:
We've determined that one or more temporary entitlement exceptions requested for
this app are not appropriate and will not be granted:
com.apple.security.temporary-exception.files.home-relative-path.read-write /
Very vague and the second time they are not telling me what is wrong with what i'm doing.
For that I use following entitlement:
<key>com.apple.security.app-sandbox</key>
<true/>
<key>com.apple.security.files.user-selected.read-write</key>
<string>True</string>
<key>com.apple.security.temporary-exception.files.home-relative-path.read-write</key>
<array>
<string>/</string>
</array>
And this is how I use the entitlement:
NSSavePanel* saveSelection = [NSSavePanel savePanel];
[saveSelection setPrompt:#"Export"];
[saveSelection setMessage:NSLocalizedString(#"Save your encrypted backup file to:",#"")];
[saveSelection setNameFieldStringValue:date];
[saveSelection beginSheetModalForWindow:kDelegate.window completionHandler:^(NSInteger result) {
if (result==NSFileHandlingPanelOKButton)
{....
}
}
I really do hope somebody can help and thanks a lot in advance!
I finally got it working by adding this to the NSSavePanel:
[saveSelection setAllowedFileTypes:[NSArray arrayWithObject:#"whatever"]];
[saveSelection setAllowsOtherFileTypes:NO];
I don't know why this made it work, but it does... at least in my app.

When an OS X app is launched by a registered URL scheme, how do you access the full URL?

I'm working on an Cocoa app which is launched/activated using URLs with a custom scheme which is registered in the Info.plist file like so:
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLName</key>
<string>Open myscheme:// URLs</string>
<key>CFBundleURLSchemes</key>
<array>
<string>myscheme</string>
</array>
</dict>
</array>
My question is, once the app is launched or activated, how do I tell what the URL was that launched the app? On iOS, this is easy with the -application:openURL:sourceApplication:annotation: method on the UIApplicationDelegate since it is passed an NSURL instance.
I want to be able to pass in data into my app with URLs like myscheme://do/something/awesome
In your app delegate's -applicationWillFinishLaunching:, do:
[[NSAppleEventManager sharedAppleEventManager] setEventHandler:self andSelector:#selector(handleAppleEvent:withReplyEvent:) forEventClass:kInternetEventClass andEventID:kAEGetURL];
And handleAppleEvent:withReplyEvent: should look something like:
- (void)handleAppleEvent:(NSAppleEventDescriptor *)event withReplyEvent:(NSAppleEventDescriptor *)replyEvent {
NSString *urlString = [[event paramDescriptorForKeyword:keyDirectObject] stringValue];
// do something with the URL string
}

Resources