#!/usr/bin/python # -*- coding: utf-8 -*- # Copyright (C) 2016 Shea G Craig # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. """ Help user set default mail client despite OS X bug. Sets the default mail reader to Apple Mail (because we configure our clients to use Outlook) using the LaunchServices framework, and then immediately log out. Displays a dialog instructing user to close open applications that may block logout from occuring, rather than execute a Volcanic Instant Death Logout. This uses code from my auto_logout project for the applescripting and alert dialog presentation. This was written to be part of an OnDemand item in Munki's Managed Software Center, using outset's on-demand feature to run as the current console user (as it would otherwise run as root and not work). """ import os import subprocess import sys # pylint: disable=no-name-in-module from AppKit import (NSImage, NSAlert, NSTimer, NSRunLoop, NSApplication, NSSound, NSModalPanelRunLoopMode, NSApp, NSRunAbortedResponse, NSAlertFirstButtonReturn) # pylint: enable=no-name-in-module from LaunchServices import LSSetDefaultHandlerForURLScheme from SystemConfiguration import SCDynamicStoreCopyConsoleUser # Sound played when alert is presented. See README. ALERT_SOUND = "Submarine" # Icon used in the alerts. If not present, the Python rocket is used # instead. ICON = "/usr/local/sas/sas.png" # Methods are named according to PyObjC/Cocoa style. # pylint: disable=invalid-name class Alert(NSAlert): """Subclasses NSAlert to include a timeout.""" def init(self): # pylint: disable=super-on-old-class """Add an instance variable for our timer.""" self = super(Alert, self).init() self.timer = None self.alert_sound = None return self def setIconWithContentsOfFile_(self, path): """Convenience method for adding an icon. Args: path: String path to a valid NSImage filetype (png) """ icon = NSImage.alloc().initWithContentsOfFile_(path) self.setIcon_(icon) # pylint: disable=no-member def setAlertSound_(self, name): """Set the sound to play when alert is presented. Args: name: String name of a system sound. See the README. """ self.alert_sound = name def setTimeToGiveUp_(self, time): """Configure alert to give up after time seconds.""" # Cocoa objects must use class func alloc().init(), so pylint # doesn't see our init(). # pylint: disable=attribute-defined-outside-init self.timer = \ NSTimer.timerWithTimeInterval_target_selector_userInfo_repeats_( time, self, "_killWindow", None, False) # pylint: enable=attribute-defined-outside-init def present(self): """Present the Alert, giving up after configured time.. Returns: Int result code, based on PyObjC enums. See NSAlert Class reference, but result should be one of: User clicked the cancel button: NSAlertFirstButtonReturn = 1000 Alert timed out: NSRunAbortedResponse = -1001 """ if self.timer: NSRunLoop.currentRunLoop().addTimer_forMode_( self.timer, NSModalPanelRunLoopMode) # Start a Cocoa application by getting the shared app object. # Make the python app the active app so alert is noticed. app = NSApplication.sharedApplication() app.activateIgnoringOtherApps_(True) if self.alert_sound: sound = NSSound.soundNamed_(self.alert_sound).play() result = self.runModal() # pylint: disable=no-member print result return result # pylint: disable=no-self-use def _killWindow(self): """Abort the modal window as managed by NSApp.""" NSApp.abortModal() # pylint: enable=no-self-use # pylint: enable=no-init # pylint: enable=invalid-name def build_alert(): """Build an alert for auto-logout notifications.""" alert = Alert.alloc().init() # pylint: disable=no-member alert.setMessageText_( "Setting the default mail reader requires an immediate logout " "due to a bug in OS X.") alert.setInformativeText_("Please quit all applications and hit 'Okay'. " "Your computer will then logout.") alert.addButtonWithTitle_("Okay") alert.addButtonWithTitle_("Cancel") alert.setIconWithContentsOfFile_(ICON) alert.setAlertSound_(ALERT_SOUND) return alert def set_mail_reader(bundle_id): """Use LaunchServices to set mailto handler. There is a bug in OS X that allows you to set this only once. Afterwards, if you set it again, it will revert to the previous setting within about 10 seconds. Until this is fixed, logging out really quickly seems to work around it. Args: bundle_id (String): Bundle Identifier for the app to handle mail. Caps do not seem to matter. Returns: Integer return code (0 is a success) as per https://developer.apple.com/library/mac/documentation/Carbon/Reference/LaunchServicesReference/ """ return LSSetDefaultHandlerForURLScheme("mailto", bundle_id) def really_log_out(): """Log out without the prompt. Will still ask about open apps.""" run_applescript('tell application "loginwindow" to «event aevtrlgo»') def run_applescript(script): """Run an applescript""" process = subprocess.Popen(['osascript', '-'], stdout=subprocess.PIPE, stdin=subprocess.PIPE, stderr=subprocess.PIPE) result, err = process.communicate(script) if err: raise Exception(err) return process.returncode def build_abort_alert(): """Build an alert for letting user know it failed.""" alert = Alert.alloc().init() # pylint: disable=no-member alert.setMessageText_( "Failed to set default mail handler") alert.setInformativeText_("Please contact the Helpdesk.") alert.addButtonWithTitle_("Okay") alert.addButtonWithTitle_("Cancel") alert.setIconWithContentsOfFile_(ICON) alert.setAlertSound_(ALERT_SOUND) return alert def main(): alert = build_alert() if alert.present() != NSAlertFirstButtonReturn: print "User Cancelled" sys.exit() else: if set_mail_reader("com.apple.mail") == 0: really_log_out() else: abort_alert = build_abort_alert() alert.present() sys.exit(1) if __name__ == "__main__": main()