AppleScript is a great tool. It is awesome to be able to get the selected text from an application, grab the current URL from Safari, ask the user to choose a file, or show a dialog box requesting text. But writing AppleScript scripts is usually painful.
For anything mildly complicated, I would much rather write something in Python. So a lot of my AppleScripts look like this:
do shell script some_python_or_bash_script
For yet another time, I recently found myself making an AppleScript where part 3 of the process involved composing an email to someone. It is difficult to take the result of the shell script (which is just a single, structureless string) and parse out multiple fields (body, subject, recipient) to pass to a complicated make new message
command.
So instead, I made a Python wrapper around the make new message
AppleScript command. Yes, that means I am using AppleScript to call a shell script which runs an AppleScript, but I’m okay with that. (Others have done the same thing, but not with the full set of options that I wanted.)
There are already command line mail programs. Why not just use one of them? Two reasons.
First, getting mail to transfer properly is always a pain. Comcast won’t let you use their SMTP, and if they did, your message would probably be marked as spam. So you have to figure out how to hook authenticated SMTP up to Google, and then it breaks, and you just get sick of it. Currently, my best solution to this has been to pipe a message over SSH to my work computer, which has a fully functional transfer agent, just to send an email to myself!
Second, and more important, often you want to see the message and maybe edit it a little before you send it. This also minimizes the chance that a script will screw up and either not send the mail or send duplicates.
AppleScript to create a mail message looks about like this:
tell application "Mail"
make new outgoing message with properties {visible:true,¬
subject:"Happy Birthday!",content:"The big 60!"}
tell result
make new to recipient with properties {address:"[email protected]"}
make new attachment with properties {file name:"cake.jpg"}
end tell
end tell
The first half of the Python script does nothing more than create an AppleScript and feed it to the osascript
command.
#!/usr/bin/python
import sys
import argparse
import os.path
from subprocess import Popen,PIPE
def escape(s):
"""Escape backslashes and quotes to appease AppleScript"""
s = s.replace("\\","\\\\")
s = s.replace('"','\\"')
return s
def make_message(content,subject=None,to_addr=None,from_addr=None,
send=False,cc_addr=None,bcc_addr=None,attach=None):
"""Use applescript to create a mail message"""
if send:
properties = ["visible:false"]
else:
properties = ["visible:true"]
if subject:
properties.append('subject:"%s"' % escape(args.s))
if from_addr:
properties.append('sender:"%s"' % escape(args.r))
if len(content) > 0:
properties.append('content:"%s"' % escape(content))
properties_string = ",".join(properties)
template = 'make new %s with properties {%s:"%s"}'
make_new = []
if to_addr:
make_new.extend([template % ("to recipient","address",
escape(addr)) for addr in to_addr])
if cc_addr:
make_new.extend([template % ("cc recipient","address",
escape(addr)) for addr in cc_addr])
if bcc_addr:
make_new.extend([template % ("bcc recipient","address",
escape(addr)) for addr in bcc_addr])
if attach:
make_new.extend([template % ("attachment","file name",
escape(os.path.abspath(f))) for f in attach])
if send:
make_new.append('send')
if len(make_new) > 0:
make_new_string = "tell result\n" + "\n".join(make_new) + \
"\nend tell\n"
else:
make_new_string = ""
script = """tell application "Mail"
make new outgoing message with properties {%s}
%s end tell
""" % (properties_string, make_new_string)
# run applescript
p = Popen('/usr/bin/osascript',stdin=PIPE,stdout=PIPE)
p.communicate(script) # send script to stdin
return p.returncode
Dr. Drang recently complained about how inconvenient it is to send data to a subprocess in Python. I feel his pain, because I have spent plenty of time and trial and error to figure out how Popen
and communicate
work. The official documentation is no help, either.
In the end, though, there is nothing terribly ugly about the three lines that run the AppleScript. If you want to send anything to the subprocess’s stdin
, you need the argument stdin=PIPE
(or =subprocess.PIPE
, depending on your import statement). Running communicate
returns a tuple with the subprocess’s stdout
and stderr
, but only if you use the arguments stdout=PIPE
and stderr=PIPE
. So my script, communicate only returns the stdout
(which I discard).
When you don’t specify stderr=PIPE
, the error output is just passed along to the main process’s stderr
(and so also with stdout
). If you run my script from the command line, any errors from the osascript
command will just be printed on your screen (unless, of course, you do something like 2>foo
).
My newest rule to myself is “Never parse your own command line arguments.” Especially when I make something that I only ever plan to call from other scripts, and nobody but me is ever going to see, it is very tempting to do something stupid like require 8 positional arguments in a specific order.
Then you change some script somewhere and everything breaks. Or you want to use the script again and there is no --help
. So you have to jump into source that you wrote a year ago just to figure out what to do. Not good.
The argparse library is new and replaces the short-lived and now depreciated optparse. But it has lots of useful bells and whistles. For example, with the type=argparse.FileType()
option, you can add an argument that expects a filename and automatically opens the file for you. It also creates a --help
option automatically.
Here is the second half of the script.
def parse_arguments():
parser = argparse.ArgumentParser(
description="Create a new mail message using Mail.app")
parser.add_argument('recipient',metavar="to-addr",nargs="*",
help="message recipient(s)")
parser.add_argument('-s',metavar="subject",help="message subject")
parser.add_argument('-c',metavar="addr",nargs="+",
help="carbon copy recipient(s)")
parser.add_argument('-b',metavar="addr",nargs="+",
help="blind carbon copy recipient(s)")
parser.add_argument('-r',metavar="addr",help="from address")
parser.add_argument('-a',metavar="file",nargs="+",
help="attachment(s)")
parser.add_argument('--input',metavar="file",help="Input file",
type=argparse.FileType('r'),default=sys.stdin)
parser.add_argument('--send',action="store_true",
help="Send the message")
return parser.parse_args()
if __name__ == "__main__":
args = parse_arguments()
content = args.input.read()
code = make_message(
content,
subject = args.s,
to_addr = args.recipient,
from_addr = args.r,
send = args.send,
cc_addr = args.c,
bcc_addr = args.b,
attach = args.a)
sys.exit(code)
When you run parse_args
, it returns a special Namespace
object, which has the parsed arguments as attributes. (Why didn’t they use a dictionary?) In my script, “recipient”, which is a positional argument because it lacks a leading hyphen, is stored in args.recipient
. The subject is stored in args.s
. If I wanted to, I could pass ["--subject","-s"]
to add_argument
, and then the subject would be stored in args.subject
, but could be specified on the command line as either -s subject
or --subject subject
. With the action="store_true"
argument, args.send
will be true if the user gives the --send
option, and false otherwise.
I call the script mailapp. Just run
$ ls | mailapp -s "Here's how my home directory looks"