Skip to content

Instantly share code, notes, and snippets.

@sheagcraig
Last active October 26, 2017 21:24
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save sheagcraig/4fa2a11b7f1738e11c79 to your computer and use it in GitHub Desktop.
Save sheagcraig/4fa2a11b7f1738e11c79 to your computer and use it in GitHub Desktop.
Munki/Outset/PyObjC Self Service Set Apple Mail as Default Handler for mailto
# Put this at /usr/local/share/luggage/luggage.local to reap the rewards.
USE_PKGBUILD=1
l_usr_local_outset: l_usr_local
@sudo mkdir -p ${WORK_D}/usr/local/outset/{firstboot-packages,firstboot-scripts,everyboot-scripts,login-every,login-once,on-demand}
@sudo chown -R root:wheel ${WORK_D}/usr/local/outset
@sudo chmod -R 755 ${WORK_D}/usr/local/outset
pack-outset-firstboot-packages-%: % l_usr_local_outset
@sudo ${INSTALL} -m 755 -g wheel -o root "${<}" ${WORK_D}/usr/local/outset/firstboot-packages
pack-outset-firstboot-scripts-%: % l_usr_local_outset
@sudo ${INSTALL} -m 755 -g wheel -o root "${<}" ${WORK_D}/usr/local/outset/firstboot-scripts
pack-outset-everyboot-scripts-%: % l_usr_local_outset
@sudo ${INSTALL} -m 755 -g wheel -o root "${<}" ${WORK_D}/usr/local/outset/everyboot-scripts
pack-outset-login-every-%: % l_usr_local_outset
@sudo ${INSTALL} -m 755 -g wheel -o root "${<}" ${WORK_D}/usr/local/outset/login-every
pack-outset-login-once-%: % l_usr_local_outset
@sudo ${INSTALL} -m 755 -g wheel -o root "${<}" ${WORK_D}/usr/local/outset/login-once
pack-outset-on-demand-%: % l_usr_local_outset
@sudo ${INSTALL} -m 755 -g wheel -o root "${<}" ${WORK_D}/usr/local/outset/on-demand
include /usr/local/share/luggage/luggage.make
TITLE=set_apple_mail_handler
REVERSE_DOMAIN=com.sas
PAYLOAD=\
pack-usr-local-sas-sas.png \
pack-script-postinstall \
pack-outset-on-demand-set_apple_mail_handler.py \
PACKAGE_VERSION=1.0.0
pack-usr-local-sas-%: % l_usr_local_sas
@sudo ${INSTALL} -m 755 -g wheel -o root "${<}" ${WORK_D}/usr/local/sas
l_usr_local_sas: l_usr_local
@sudo mkdir -p ${WORK_D}/usr/local/sas
@sudo chown -R root:wheel ${WORK_D}/usr/local/sas
@sudo chmod -R 755 ${WORK_D}/usr/local/sas
#!/bin/bash
touch /private/tmp/.com.github.outset.ondemand.launchd
<?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>OnDemand</key>
<true/>
<key>_metadata</key>
<dict>
<key>created_by</key>
<string>shcrai</string>
<key>creation_date</key>
<date>2016-01-21T21:03:53Z</date>
<key>munki_version</key>
<string>2.4.0.2561</string>
<key>os_version</key>
<string>10.11.3</string>
</dict>
<key>autoremove</key>
<false/>
<key>catalogs</key>
<array>
<string>production</string>
</array>
<key>category</key>
<string>Self Service</string>
<key>description</key>
<string>Due to a bug in OS X, the default mail handler cannot be set by the user after it has been set once without programmatic intervention. To restore Apple Mail as the default mail reader, run this item, and then following the prompting instructions, quit all of your open applications prior to continuing. This change requires a logout.</string>
<key>developer</key>
<string>SAS</string>
<key>display_name</key>
<string>Set Default Mail Handler to Apple Mail</string>
<key>icon_hash</key>
<string>d422e6891ec0affeecfb018fde3d64754c6ef5b71bc61e1cac18dc254cb7a25e</string>
<key>icon_name</key>
<string>sas.png</string>
<key>installed_size</key>
<integer>20</integer>
<key>installer_item_hash</key>
<string>4adab62eac994be227667bc855885aa74a3fefc2d33c74e5fba0fbe76f22f12e</string>
<key>installer_item_location</key>
<string>config/set_apple_mail_handler-1.0.0.pkg</string>
<key>installer_item_size</key>
<integer>12</integer>
<key>minimum_os_version</key>
<string>10.5.0</string>
<key>name</key>
<string>set_apple_mail_handler</string>
<key>receipts</key>
<array>
<dict>
<key>installed_size</key>
<integer>20</integer>
<key>packageid</key>
<string>com.sas.set_apple_mail_handler</string>
<key>version</key>
<string>1.0.0</string>
</dict>
</array>
<key>requires</key>
<array>
<string>outset</string>
</array>
<key>uninstall_method</key>
<string>removepackages</string>
<key>uninstallable</key>
<true/>
<key>version</key>
<string>1.0.0</string>
</dict>
</plist>
#!/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()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment