├── .gitignore ├── README.org ├── handler ├── __init__.py └── mutt.py ├── setup.py ├── url-open-handler.cfg.example └── url-open-handler.py /.gitignore: -------------------------------------------------------------------------------- 1 | mutt_message_handler-Info.plist 2 | -------------------------------------------------------------------------------- /README.org: -------------------------------------------------------------------------------- 1 | * url-open-handler 2 | Registers a handler for URL Apple Events and calls python code or 3 | programs with the URL. 4 | 5 | It's main purpose is to direct message://MESSAGE_ID URLs to mutt, 6 | similar to what [[http://mailtomutt.sourceforge.net/][MailtoMutt]] does for mailto:// URLs. 7 | 8 | In the default configuration it listens for /message/ and /mailto/ 9 | URLs. This tool does not register as a default handler for this 10 | protocols. To register URL handler have a look at 11 | [[http://www.rubicode.com/Software/RCDefaultApp/][Default App]]. 12 | 13 | ** Dependencies 14 | - python 2.7 15 | - pyobjc 16 | 17 | ** Building 18 | : python setup.py py2app 19 | 20 | ** Installing 21 | Copy =url-open-handler.app= from =dist/= to your =Applications= folder 22 | 23 | ** Running 24 | =url-open-handler.app= runs in background without a Dock Icon. I 25 | suggest you start =url-open-handler.app= with OSX. 26 | 27 | ** Configuration 28 | Copy =url-open-handler.cfg.example= to 29 | =~/Library/Preferences/url-open-handler.cfg= and change it to your 30 | needs. 31 | 32 | A section header represents the protocol part of an URL. In each 33 | section you can specify either a program to execute or a python 34 | module to load and a function to call from that module. See the 35 | example configuration file for details. 36 | 37 | ** How to add protocols (schemas) 38 | Add an item to the =CFBundleURLSchemes= list in setup.py and rebuild. 39 | 40 | ** Handler 41 | *** Mutt message handler 42 | The package comes with a message:// handler for mutt. It has code to 43 | run a new mutt session in iTerm.app or open the mail in an existing 44 | mutt session. 45 | 46 | The handler uses the full-text mail search tool =notmuch= to lookup 47 | path to message ids. 48 | -------------------------------------------------------------------------------- /handler/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/irq0/osx-url-open-handler/a1d9e3db2bf9cfaa4167db48b85bb1ce7ad907be/handler/__init__.py -------------------------------------------------------------------------------- /handler/mutt.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # Open message:// protocol in new mutt session 4 | 5 | 6 | import subprocess 7 | import json 8 | import os.path 9 | import sys 10 | import re 11 | import struct 12 | import time 13 | import urllib 14 | 15 | from appscript import * 16 | 17 | MUTT="/Users/seri/homebrew/bin/mutt" 18 | 19 | def quote_filename(filename): 20 | return filename.replace(" ", "\\ ") 21 | 22 | def get_message(message_id=None): 23 | result = None 24 | out = subprocess.check_output(["notmuch", "search", "--format=json", 25 | "--output=files", "--limit=1", 26 | "id:{message_id}".format(message_id=message_id)]) 27 | filenames = json.loads(out) 28 | 29 | if filenames and len(filenames) > 0: 30 | result = { 31 | "mailbox" : quote_filename(get_mailbox_path(filenames[0])), 32 | "id" : message_id, 33 | } 34 | 35 | if not os.path.isdir(result["mailbox"]): 36 | print "Mailbox {0} is not a directory!".format(result) 37 | result = None 38 | 39 | return result 40 | 41 | def get_mailbox_path(filename): 42 | def find_path(head, tail): 43 | if tail in ("cur", "new", "tmp"): 44 | return head 45 | elif not head: 46 | return None 47 | else: 48 | return find_path(*os.path.split(head)) 49 | 50 | return find_path(*os.path.split(filename)) 51 | 52 | 53 | def iterm_sess_dict(sess): 54 | return { 55 | "name" : sess.name(), 56 | "tty" : sess.tty(), 57 | "session" : sess, 58 | } 59 | 60 | def iterm_sessions(): 61 | result = [] 62 | 63 | for term in app("iTerm").terminals(): 64 | for sess in term.sessions(): 65 | result.append(iterm_sess_dict(sess)) 66 | return result 67 | 68 | def start_new_mutt_cmd(message): 69 | return [MUTT, 70 | "-f", message["mailbox"], 71 | "-z", 72 | "-e", "push ~i\"{0}\"".format(message["id"])] 73 | 74 | def mutt_intern_select_cmd(message): 75 | return """\ 76 | : push {mailbox}\ 77 | ~i\"{id}\"""".format(**message) 78 | 79 | 80 | def cmd_seq_to_str(seq): 81 | result = seq[0] 82 | result += " " 83 | result += " ".join(map(lambda x: "\"{0}\"".format(x), seq[1:])) 84 | return result 85 | 86 | def open_new_mutt_here(message): 87 | subprocess.call(start_new_mutt_cmd(message)) 88 | 89 | def open_new_mutt_in_iterm(message): 90 | ap = app("iTerm") 91 | ap.activate() 92 | sess = ap.current_terminal().sessions.end.make(new=k.session) 93 | sess.name.set("mutt") 94 | sess.exec_(command=cmd_seq_to_str(start_new_mutt_cmd(message))) 95 | 96 | def open_mail_in_existing_mutt(message, fallback=open_new_mutt_in_iterm): 97 | sess = filter(lambda x : x["name"] == "mutt", iterm_sessions()) 98 | 99 | if len(sess) == 0: 100 | print "No mutt sessions found -> opening new one" 101 | fallback(message) 102 | else: 103 | session = sess[0]["session"] 104 | print "Opening mail in session: {0}".format(sess[0]["tty"]) 105 | 106 | session.write(text=mutt_intern_select_cmd(message)) 107 | 108 | def unquote_mid(mid): 109 | result = urllib.unquote(mid) 110 | result = result.replace("<","").replace(">","") 111 | return result 112 | 113 | def handle_message(mid, func=open_mail_in_existing_mutt): 114 | from Foundation import NSLog 115 | 116 | mid = unquote_mid(mid) 117 | msg = get_message(mid) 118 | if not msg: 119 | NSLog("No message for %@ found!", mid) 120 | else: 121 | NSLog("Opening message %@", msg) 122 | func(get_message(mid)) 123 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | """ 2 | This is a setup.py script generated by py2applet 3 | 4 | Usage: 5 | python setup.py py2app 6 | """ 7 | 8 | from setuptools import setup 9 | 10 | APP = ['url-open-handler.py'] 11 | DATA_FILES = ['handler'] 12 | OPTIONS = { 13 | 'argv_emulation': False, 14 | 'plist' : { 15 | 'CFBundleDevelopmentRegion' : 'en', 16 | 'NSPrincipalClass' : 'NSApplication', 17 | 'NSAppleScriptEnabled' : 'YES', 18 | 'LSUIElement' : 'YES', 19 | 'CFBundleIdentifier' : 'org.irq0.custom_url_handler', 20 | 'CFBundleURLTypes' : [{ 21 | 'CFBundleURLName' : 'eMail Message', 22 | 'CFBundleURLSchemes' : [ 23 | 'message', 24 | 'mailto', 25 | ] 26 | }] 27 | } 28 | } 29 | 30 | setup( 31 | app=APP, 32 | data_files=DATA_FILES, 33 | options={'py2app': OPTIONS}, 34 | setup_requires=['py2app'], 35 | ) 36 | -------------------------------------------------------------------------------- /url-open-handler.cfg.example: -------------------------------------------------------------------------------- 1 | # Handle protocol "message" with mutt handler 2 | [message] 3 | module = handler.mutt 4 | func = handle_message 5 | 6 | # Handle protocol "mailto" by calling program with 7 | # hierarchical part of the url as argument 8 | [mailto] 9 | run = ~/bin/test.sh 10 | -------------------------------------------------------------------------------- /url-open-handler.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | import struct 4 | import os.path 5 | import subprocess 6 | import re 7 | import importlib 8 | 9 | from objc import YES, NO, nil, signature 10 | from AppKit import * 11 | from Foundation import * 12 | from PyObjCTools import NibClassBuilder, AppHelper 13 | 14 | import ConfigParser 15 | 16 | config = ConfigParser.ConfigParser() 17 | 18 | def get_script_for_scheme(scheme): 19 | script = os.path.expanduser(config.get(scheme, 'run')) 20 | if not os.path.isfile(script): 21 | raise Exception("Config Error: Script for {0} is not a file".format(scheme)) 22 | 23 | return script 24 | 25 | def get_py_module_for_scheme(scheme): 26 | result = config.get(scheme, 'module') 27 | return result 28 | 29 | def get_py_func_for_scheme(scheme): 30 | result = config.get(scheme, 'func') 31 | return result 32 | 33 | def get_run_func(scheme): 34 | return [run_script, run_python][int(config.has_option(scheme, 'module'))] 35 | 36 | def run_script(scheme_name, hier_part): 37 | NSLog("Running script handler") 38 | subprocess.call([get_script_for_scheme(scheme_name), hier_part]) 39 | 40 | def run_python(scheme_name, hier_part): 41 | mod_name = get_py_module_for_scheme(scheme_name) 42 | func_name = get_py_func_for_scheme(scheme_name) 43 | 44 | NSLog("Running python handler mod=%@ func=%@", mod_name, func_name) 45 | 46 | mod = importlib.import_module(mod_name) 47 | func = getattr(mod, func_name) 48 | 49 | func(hier_part) 50 | 51 | def run_url(schema_name, hier_part): 52 | get_run_func(schema_name)(schema_name, hier_part) 53 | 54 | class AppDelegate(NSObject): 55 | 56 | def applicationWillFinishLaunching_(self, notification): 57 | man = NSAppleEventManager.sharedAppleEventManager() 58 | man.setEventHandler_andSelector_forEventClass_andEventID_( 59 | self, 60 | "openURL:withReplyEvent:", 61 | struct.unpack(">i", "GURL")[0], 62 | struct.unpack(">i", "GURL")[0]) 63 | man.setEventHandler_andSelector_forEventClass_andEventID_( 64 | self, 65 | "openURL:withReplyEvent:", 66 | struct.unpack(">i", "WWW!")[0], 67 | struct.unpack(">i", "OURL")[0]) 68 | NSLog("Registered URL handler") 69 | 70 | @signature('v@:@@') 71 | def openURL_withReplyEvent_(self, event, replyEvent): 72 | keyDirectObject = struct.unpack(">i", "----")[0] 73 | url = event.paramDescriptorForKeyword_(keyDirectObject).stringValue().decode('utf8') 74 | 75 | urlPattern = re.compile(r"^(.*?)://(.*)$") 76 | match = urlPattern.match(url) 77 | 78 | schema = match.group(1) 79 | hier_part = match.group(2) 80 | 81 | NSLog("Received URL: %@", url) 82 | run_url(schema, hier_part) 83 | 84 | def main(): 85 | config.read(os.path.expanduser('~/Library/Preferences/url-open-handler.cfg')) 86 | 87 | app = NSApplication.sharedApplication() 88 | 89 | delegate = AppDelegate.alloc().init() 90 | app.setDelegate_(delegate) 91 | 92 | AppHelper.runEventLoop() 93 | 94 | if __name__ == '__main__': 95 | main() 96 | --------------------------------------------------------------------------------