Automatically print out incoming eMails and attachments in Apple Mail

I was often asked, if I could provide a simple solution to print out incoming eMails and attachments in Apple Mail. Well, so far I couldn’t, as Apple Mail unfortunately doesn’t support to automatically print documents without invoking the print dialog.

But then my employer also needed such a solution in order to automatically process incoming work orders and print their PDF attachments to a network printer in the production hall. And so I had to take a closer look at how to automatically print eMails and attachments in Apple Mail. After happily spending an entire morning in front of my office iMac, I am proud to share my little AppleScript snippet with you. May it also make your future workdays a little bit easier:

[center]pergamail - Automatically print incoming eMail messages and attachments in AppleMail (ca. 35.3 KB)
(v0.1b, pergamail is BETA software)[/center]

An eMail message printed out by pergamail might look like that:

Requirements
pergamail was tested under/with:
¢ Mac OS X 10.5.1
¢ Apple Mail 3.1
¢ Intel & PowerPC based Macs

Installation
To install pergamail on your Mac, you must first download it and then create a custom mail rule in Apple Mail. In this mail rule you have to choose to run an AppleScript as shown below:

All rule-matching eMails will then be processed by pergamail and - hopefully - printed out accordingly.

Configuration
pergamail offers some advanced options, which you can directly set in its source code. Just open the script in the Script Editor and have a look at the first lines. When making changes to pergamail, please always save it as a «Script bundle», as it also contains a little Python script, which translates byte sizes into a human readable form.

«defprintapp»

 
property defprintapp : "TextEdit" 
-- default value: "TextEdit" 
-- possible values: "lpr" or "TextEdit" 

You can set defprintapp to print either via the shell using the «lpr»-command or via TextEdit, which can also automatically print documents using AppleScript without invoking the print dialog. If TextEdit was not running on your Mac at the time pergamail started its work, it will quit it afterwards.

«defprintername»

 
property defprintername : missing value 
-- default value: missing value 
-- possible values: any name of an available printer, e.g. "HP LaserJet 1300" 

By default, pergamail is printing to the currently set printer. But if you always want to print to a specific printer, just provide the printer name here. After pergamail is finished, it will always set back your printer to the one used before.

«defprintatms»

 
property defprintatms : false 
-- default value: false 
-- possible values: false or true 

If you want to try to also print out the attachments of the eMail messages, then set this value to true. But please note that pergamail can only print attachments, which can be printed out using the «lpr»-command (PS, PDF, RTF(D), TXT, HTML, most image formats natively supported by Mac OS X).

«defmsginfo»


property defmsginfo : "short" 
-- default value: "short" 
-- possible values: "short" or "long" 

If you set this property to “long”, some more infos will show up in the mail header, e.g. message size.

Notes
Please note, that pergamail currently doesn’t support fancy HTML eMail very good. You won’t see any pictures or formatted text in the printouts from pergamail. Maybe this can be overcome in the future by parsing the raw source of the eMail messages with a Python/Ruby/Perl script, then extracting and saving the HTML part, and finally printing it with the «lpr»-command. Maybe.

Moreover I encountered some problems, when I tried to use pergamail with my Bonjour-shared HP network printer at home. But it runs just fine in the company. So if you encounter any problems, please remember that pergamail is currently just a little beta script. It’s not an expensive and mature application.

Important: If pergamail encounters an error, it creates a log file named «pergamail.log» on your desktop containing the last error message.

With a little bit of AppleScript knowledge (maybe even without) you can easily adjust the pergamail eMail output to your own requirements. Just look for the «msgtotxt» function.

Future
Currently pergamail creates text files from incoming eMail messages, but it would be much better to directly create HTML or PDF documents to get more control over the print layout. previmail alreday uses an advanced HTML template and Colendar shows hot to create a PDF on the fly, so I might just need to invest another weekend to write an improved version of pergamail :smiley:

Last but not least, all parts of pergamail can be inspected with the Script Editor. I invite everybody to modify, enhance and misuse pergamail for their own purposes.

Happy eMail printing!

Important: Opening and saving the below script code in Script Editor won’t result in a usable AppleScript! That is because pergamail internally relies on a Python script, which is located inside its Script bundle. Therefor please download the complete script here.


-- created: 05.02.2008

-- Installed together with an AppleScript-based Apple Mail mail rule,
-- this script - hopefully - prints out all rule-matching eMails (and their
-- attachments according to the options manually set below)

-- The script was tested under/with:
-- ¢ Mac OS X 10.5.1
-- ¢ Apple Mail 3.1
-- ¢ Intel & PowerPC based Macs

property mytitle : "pergamail"
property myversion : "0.1b"

-- here you can set the application which is going to
-- print the eMail message: TextEdit or lpr
-- the default value is 'TextEdit'
-- why 'TextEdit'? because it can print without
-- invoking the print dialog
-- lpr is pure printing via the command line
-- possible values: "TextEdit" or "lpr"
property defprintapp : "TextEdit"
-- you can choose a default printer to be used, e.g.
-- "HP LaserJet 1300"
-- default value is missing value
property defprintername : missing value
-- do you also want to try to print the eMail attachments?
-- please note, that you can currently only print
-- attachments of the following types (due to the use of the 'lpr'-command):
-- PDF, RTF(D), TXT, HTML, most image formats natively
-- supported by Mac OS X
property defprintatms : false
-- you can set this property to 'long' to see even more mail infos
-- in the plain text report
property defmsginfo : "short"

-- unique handler to perform AppleScript actions on rule-matching Apple Mail messages
using terms from application "Mail"
	on perform mail action with messages matchmsgs for rule therule
		repeat with matchmsg in matchmsgs
			my printmailmsg(matchmsg)
		end repeat
	end perform mail action with messages
end using terms from

-- deactivated debug routine 
(*on run
	tell application "Mail"
		set msg to item 1 of (selection as list)
		my printmailmsg(msg)
	end tell
end run*)

-- main function controlling the script procedure
on printmailmsg(msg)
	try
		-- converting the Apple Mail message to a plain text report
		--set msgtxt to "Hallo"
		set msgtxt to my msgtotext(msg)
		-- finding and getting an unused temp file
		set tmpfilepath to my TmpFile's newpath()
		-- saving the plain text report in the temp file as UTF-8
		my writetofile(msgtxt, tmpfilepath)
		-- printing the temp file
		my printfile(tmpfilepath, defprintapp)
		-- deleting the temp file
		my TmpFile's remove()
		-- printing the attached files if possible
		if defprintatms is true then
			my printatms(msg)
		end if
		-- THE END
	on error errmsg number errnum
		set logmsg to ((current date) as Unicode text) & ": " & errmsg & " (" & errnum & ")"
		set logfile to ((path to desktop) as Unicode text) & "pergamail.log"
		my writetofile(logmsg, logfile)
	end try
end printmailmsg

-- I am printing the given file with the chosen application
on printfile(filepath, printapp)
	-- if the user chose to use a default printer,
	-- we must know the current printer, as we might have
	-- to set it back later
	if defprintername is not missing value then
		set curprintername to my getcurprintername()
		my setcurprinterbyname(defprintername)
	end if
	-- printing via the command line
	if printapp is "lpr" then
		-- silence is golden...
		set cmd to "lpr " & quoted form of (POSIX path of filepath)
		set cmd to cmd as «class utf8»
		do shell script cmd
		-- printing via TextEdit
	else if printapp is "TextEdit" then
		-- if TextEdit is currently not running, we
		-- should quit it afterwards
		if appisrunning("TextEdit") then
			set tewasrunning to true
		else
			set tewasrunning to false
		end if
		-- printing the file without invoking the print dialog and
		-- without bringing TextEdit to the foreground
		tell application "TextEdit"
			set newdoc to open (filepath as alias)
			print newdoc without print dialog
			close newdoc
		end tell
		-- cleaning up...like mother told us
		if not tewasrunning then
			tell application "TextEdit"
				quit
			end tell
		end if
	end if
	-- setting back the default printer to its prior value
	if defprintername is not missing value then
		my setcurprinterbyname(curprintername)
	end if
end printfile

-- I am printing the attachments of a mail message if possible
on printatms(msg)
	-- saving all attachments in temporary files
	set tmpfilepaths to {}
	tell application "Mail"
		set atms to mail attachments of msg
		repeat with atm in atms
			set atmname to name of atm
			repeat
				set rndnum to random number from 1000 to 99999
				set tmpfolder to (path to temporary items folder from user domain) as Unicode text
				set tmpfilepath to tmpfolder & "tmp_" & rndnum & "_" & atmname
				try
					set tmpfilepath to tmpfilepath as alias
				on error
					exit repeat
				end try
			end repeat
			try
				save atm in tmpfilepath
				set tmpfilepaths to tmpfilepaths & tmpfilepath
			end try
		end repeat
	end tell
	-- printing the temporarily saved attachments and then deleting them afterwards
	-- not all attachment types can be printed (e.g. MS Word/Exel, etc.)
	repeat with tmpfilepath in tmpfilepaths
		my printfile(tmpfilepath, "lpr")
		tell application "Finder"
			delete (tmpfilepath as alias)
		end tell
	end repeat
end printatms

-- I am returning the name of the current printer
on getcurprintername()
	tell application "Printer Setup Utility"
		return name of current printer
	end tell
end getcurprintername

-- I am setting the current printer to the given printer name
on setcurprinterbyname(printername)
	tell application "Printer Setup Utility"
		set allprinters to every printer
		set printernames to name of every printer
		if printername is not in printernames then
			return false
		else
			if name of current printer is not equal to printername then
				set current printer to printer printername
			end if
			return true
		end if
	end tell
end setcurprinterbyname

-- I am converting an Apple Mail eMail message to a plain text report
on msgtotext(msg)
	set newline to ASCII character 10
	tell application "Mail"
		--set msgid to id of msg
		set msguid to message id of msg
		set msgsize to my getstringsize(message size of msg)
		--set msgmbox to name of mailbox of msg
		set msgcont to content of msg
		set msgsender to sender of msg
		set msgrecvd to date received of msg
		set msgsent to date sent of msg
		set msgsubj to subject of msg
		set msgtorec to my gettxtrecipients("TO", msg)
		set msgccrec to my gettxtrecipients("CC", msg)
		set msgbccrec to my gettxtrecipients("BCC", msg)
		set msgatms to my gettxtatms(mail attachments of msg)
		set msgreplyto to reply to of msg
		--set msgpath to my getmsgpath(msgid)
	end tell
	-- basic report:
	set msgtext to "+ + + + + + + + + + + + + + + + + + + + + + + + + + + +" & newline & newline
	set msgtext to msgtext & "SUBJECT: " & msgsubj & newline & "FROM: " & msgsender & newline & "DATE: " & msgrecvd & newline & "TO: " & msgtorec
	-- further enhancing the report if information is available
	-- >> CC recipients?
	if msgccrec is not missing value then
		set msgtext to msgtext & newline & "CC: " & msgccrec
	end if
	-- >> BCC recipients?
	if msgbccrec is not missing value then
		set msgtext to msgtext & newline & "BCC: " & msgbccrec
	end if
	-- >> long info?
	if defmsginfo is "long" then
		set msgtext to msgtext & newline & "SIZE: " & msgsize & newline & "SENT: " & msgsent & newline & "REPLY-TO: " & msgreplyto
	end if
	-- >> attachments?
	if msgatms is not missing value then
		set msgtext to msgtext & newline & newline & "##########" & newline & msgatms & newline & "##########"
	end if
	set msgtext to msgtext & newline & newline & "+ + + + + + + + + + + + + + + + + + + + + + + + + + + +" & newline & newline & msgcont
	return msgtext
end msgtotext

-- I am returning the recipients of an eMail message as plain text list
on gettxtrecipients(rectype, msg)
	-- I am returning the recipeints as a text list, e.g.:
	-- "Martin Michel <martin@joyofscripting.com>, Steve Jobs <steve@mac.com>"
	-- If there are no recipients availabe, I return «missing value»
	set textrecipients to ""
	tell application "Mail"
		if rectype is "TO" then
			set recpnts to to recipients of msg
		else if rectype is "CC" then
			set recpnts to cc recipients of msg
		else if rectype is "BCC" then
			set recpnts to bcc recipients of msg
		end if
		if recpnts is {} then
			return missing value
		else
			set countrecpnts to length of recpnts
			repeat with i from 1 to countrecpnts
				set recaddress to address of (item i of recpnts)
				set recname to name of (item i of recpnts)
				-- sometimes the «name» property is not available for a recipient,
				-- so we have to use an ugly try...end try-block below:
				if i is equal to countrecpnts then
					try
						set textrecipients to textrecipients & recname & " <" & recaddress & ">"
					on error
						set textrecipients to textrecipients & "<" & recaddress & ">"
					end try
				else
					try
						set textrecipients to textrecipients & recname & " <" & recaddress & ">, "
					on error
						set textrecipients to textrecipients & "<" & recaddress & ">, "
					end try
				end if
			end repeat
			return textrecipients
		end if
	end tell
end gettxtrecipients

-- I am returning the eMail attachments as a plain text list
on gettxtatms(atms)
	set textatms to ""
	if atms is {} then
		return missing value
	else
		set countatms to length of atms
		if countatms is equal to 1 then
			set textatms to ("1 Attachment:" & return)
		else
			set textatms to (countatms & " Attachments:" & return)
		end if
		tell application "Mail"
			repeat with i from 1 to countatms
				set atm to item i of atms
				set atmname to name of atm
				set atmsize to my getstringsize(file size of atm)
				set atmmime to MIME type of atm
				set atmdl to downloaded of atm
				set atmentry to "   «" & atmname & "» (" & atmsize & ")"
				if i is equal to countatms then
					set textatms to textatms & atmentry
				else
					set textatms to textatms & atmentry & return
				end if
			end repeat
		end tell
		return textatms
	end if
end gettxtatms

-- I am returning the given byte site in human readable form
on getstringsize(bytesize)
	set pyscriptpath to ((path to me) as Unicode text) & "Contents:Resources:Scripts:utl.py"
	set cmd to "python " & quoted form of (POSIX path of pyscriptpath) & space & bytesize
	set cmd to cmd as «class utf8»
	set shellresult to do shell script cmd
	return shellresult
end getstringsize

-- I am returning the file pathof the eMail message based on its message ID
on getmsgpath(msgid)
	set mailfolder to ((path to library folder from user domain) as Unicode text) & "Mail"
	set cmd to "find " & quoted form of (POSIX path of mailfolder) & space & "-name" & space & msgid & ".emlx"
	set cmd to cmd as «class utf8»
	set shellresult to do shell script cmd
	if shellresult is "" then
		return missing value
	else
		return shellresult
	end if
end getmsgpath

-- I am writing given content to a given file using UTF-8 text encoding
on writetofile(cont, filepath)
	try
		set openfile to open for access filepath with write permission
		set eof of openfile to 0
		set BOM_UTF8 to ((ASCII character 239) & (ASCII character 187) & (ASCII character 191))
		write (cont as «class utf8») to openfile
		close access openfile
		return true
	on error
		try
			close access openfile
		end try
		return false
	end try
end writetofile

-- I am indicating if a given application is currently running
on appisrunning(appname)
	tell application "System Events"
		set processnames to name of processes
	end tell
	if appname is in processnames then
		return true
	else
		return false
	end if
end appisrunning

-- script object to manage a temporary file
script TmpFile
	property filepath : missing value
	
	-- I am creating a new, not yet existing file path in the temp folder
	on newpath()
		set tmpfolderpath to (path to temporary items folder from user domain) as Unicode text
		repeat
			set rndnum to random number from 1000 to 99999
			set tmpfilepath to (tmpfolderpath & (rndnum as Unicode text) & ".tmp")
			try
				set tmpfilepath to tmpfilepath as alias
			on error
				set filepath to tmpfilepath
				exit repeat
			end try
		end repeat
		return filepath
	end newpath
	
	-- I am returning the file path of the temporary file 
	on getpath()
		return filepath
	end getpath
	
	-- I am trying to delete the temporary file
	on remove()
		try
			set command to "rm " & quoted form of (POSIX path of (filepath as Unicode text))
			do shell script command
		end try
	end remove
end script

Here’s my take on the idea. :slight_smile:

The script’s in two parts: an application-specific part tailored to the e-mail client and a vanilla part which takes care of the formatting and printing. The first part obviously needs to extract all the details from the e-mail message and then pass them on to the second part. In posh programming, the second part might be loaded into the first as a library, but I’ve written them as one to show the code. The first part’s written for PowerMail here, but I imagine that other clients can easily be scripted to deliver the same information.

The second part stitches together an RTF document with the subject, date, sender, and recipient(s) listed in a shaded box at the top. If there are any attachments, these are listed in an unshaded box which can be either above or below the body text, according to the preferences set in the script properties.

As with Martin’s script, some configuration is possible via the properties, but no effort has been made to handle HTML code. PowerMail messages have an ‘HTML content’ property which sounds useful, but I don’t want to waste any more paper on this script!

The script doesn’t require any third-party add-ons and I believe it’ll work on any system from Tiger to the present, although special measures will be required in Tiger to compile the Unicode-only characters in the ‘translationTable’ property.

Edit: I’ve partially revamped the script to make it easier to adapt for Mail. It also shows any Reply-to header that’s different from the From header. The convertByteSize() handler now takes an extra parameter for the number of decimal places’ precision and there are a few more characters in the translation table.

For Mail users, the section between the two rows of commented asterisks can be replace with the code in the following post.


property prefPrintAgent : "lpr" -- or "lp". With, "lpr" the temporary .pdf file is deleted after printing.
property prefPrinterName : missing value -- Use 'missing value' for the default printer.
property prefPrintMessage : true -- Print the message or not?
property prefPrintAttachments : false -- Print attachments or not?
property prefPageSize : "A4" -- or "US Letter".
-- property prefDuplexForMessage : false -- Not implemented.
property prefShowAttachmentsAtTop : false -- Print attachment names above the body text? Below if false.
property prefKBSize : 1024 -- 1024 or 1000. Use 'missing value' to agree with the current system.
property prefMaxDecPlacesInSizes : 2 -- Round to this number of decimal places in size displays. (.5 away from zero.)

-- Non-ASCII -> RTF translation table. Add any characters you're likely to need. (Hyphens are made non-breaking.)
property translationTable : {{"\\", "\\\\"}, {"{", "\\{"}, {"}", "\\}"}, {"-", "\\_"}, {"€", "\\'80"}, {".", "\\'85"}, {"˜", "\\lquote "}, {"'", "\\rquote "}, {""", "\\ldblquote "}, {""", "\\rdblquote "}, {"¢", "\\bullet"}, {"“", "\\endash "}, {"”", "\\emdash "}, {«data utxt00A0», "\\~"}, {"¢", "\\'a2"}, {"£", "\\'a3"}, {"«", "\\'ab"}, {"¬", "\\'ac"}, {"°", "\\'b0"}, {"º", "\\'ba"}, {"»", "\\'bb"}, {"À", "\\'c0"}, {"Á", "\\'c1"}, {"Â", "\\'c2"}, {"Ã", "\\'c3"}, {"Ä", "\\'c4"}, {"Ã…", "\\'c5"}, {"Ç", "\\'c7"}, {"È", "\\'c8"}, {"É", "\\'c9"}, {"Ê", "\\'ca"}, {"Ë", "\\'cb"}, {"ÃŒ", "\\'cc"}, {"Í", "\\'cd"}, {"ÃŽ", "\\'ce"}, {"Ï", "\\'cf"}, {"Ñ", "\\'d1"}, {"Ã’", "\\'d2"}, {"Ó", "\\'d3"}, {"Ô", "\\'d4"}, {"Õ", "\\'d5"}, {"Ö", "\\'d6"}, {"Ø", "\\'d8"}, {"Ù", "\\'d9"}, {"Ãœ", "\\'dc"}, {"Ý", "\\'dd"}, {"ß", "\\'df"}, {"à ", "\\'e0"}, {"á", "\\'e1"}, {"â", "\\'e2"}, {"ã", "\\'e3"}, {"ä", "\\'e4"}, {"Ã¥", "\\'e5"}, {"ç", "\\'e7"}, {"è", "\\'e8"}, {"é", "\\'e9"}, {"ê", "\\'ea"}, {"ë", "\\'eb"}, {"ì", "\\'ec"}, {"í", "\\'ed"}, {"î", "\\'ee"}, {"ï", "\\'ef"}, {"ñ", "\\'f1"}, {"ò", "\\'f2"}, {"ó", "\\'f3"}, {"ô", "\\'f4"}, {"ö", "\\'f6"}, {"ø", "\\'f8"}, {"ù", "\\'f9"}, {"ü", "\\'fc"}, {"ÿ", "\\'ff"}, {"č", "\\uc0\\u269 "}, {"Ä•", "\\uc0\\u277 "}, {"Ł", "\\uc0\\u321 "}, {"Å‚", "\\uc0\\u322 "}, {"Å„", "\\uc0\\u324 "}, {"Å‘", "\\uc0\\u337 "}, {"Å™", "\\uc0\\u345 "}, {"Å ", "\\uc0\\u352 "}, {"Å¡", "\\uc0\\u353 "}, {"ů", "\\uc0\\u367 "}, {"Å´", "\\uc0\\u372 "}, {"ŵ", "\\uc0\\u373 "}, {"Ŷ", "\\uc0\\u374 "}, {"Å·", "\\uc0\\u375 "}, {"ź", "\\uc0\\u378 "}, {"ż", "\\uc0\\u380 "}, {«data utxt2028», "\\line "}, {«data utxt2029», "\\uc0\\u8233 "}, {"≠", "\\uc0\\u8800 "}, {«data utxtFFFC», "\\uc0\\u65532 "}}
-- To compile the above table in Tiger, replace the {., "\\uc0\\u."} lists with:
--{{«data utxt010D», "\\uc0\\u269 "}, {«data utxt0115», "\\uc0\\u277 "}, {«data utxt0141», "\\uc0\\u321 "}, {«data utxt0142», "\\uc0\\u322 "}, {«data utxt0144», "\\uc0\\u324 "}, {«data utxt0151», "\\uc0\\u337 "}, {«data utxt0159», "\\uc0\\u345 "}, {«data utxt0160», "\\uc0\\u352 "}, {«data utxt0161», "\\uc0\\u353 "}, {«data utxt016F», "\\uc0\\u367 "}, {«data utxt0174», "\\uc0\\u372 "}, {«data utxt0175», "\\uc0\\u373 "}, {«data utxt0176», "\\uc0\\u374 "}, {«data utxt0177», "\\uc0\\u375 "}, {«data utxt017A», "\\uc0\\u378 "}, {«data utxt017C», "\\uc0\\u380 "}, {«data utxt2028», "\\line "}, {«data utxt2029», "\\uc0\\u8233 "}, {«data utxt2260», "\\uc0\\u8800 "}, {«data utxtFFFC», "\\uc0\\u65532 "}}

(* ******************** *)

(* This section should be written for your e-mail client and the way its mail actions run AppleScripts. (The code here is for PowerMail.) It should get all the relevant data from the message and pass them to the process() handler in the form of a vanilla user record:

{subject: <The message's subject line (text)>, ¬
|time sent|: <The date/time sent (AppleScript date object)>, ¬
sender: <The sender's details from the e-mail's "From:" header (text, eg. "\"Joe Bloggs\" <joe@bloggs.com>")>, ¬
|reply to|: <The Reply-To address, if any (ditto)>, ¬
|to recipients|: <Details of 'to' recipients (list of records, eg. {{|display name|:"Fred Bloggs", |email address|:"fred@bloggs.com"}, {|display name|:"Sid Bloggs", |email address|:"loretta@bloggs.com"}})>, ¬
|cc recipients|: <Details of any 'CC' recipients (list of records ditto)>, ¬
|bcc recipients|: <Details of any 'BCC' recipients (list of records ditto)>, ¬
attachments: <Details of any attachments (list of records, eg. {{|attachment name|:"My photo.jpg", |attachment size|:23527, |attachment alias|:alias "Path:To:attachments folder:My photo.jpg"}})>, ¬
content: <The message's body text>}
*)

-- PowerMail's "Mail Filters" run AppleScripts in the normal way.
on run
	getMessageData()
	process(result)
end run

-- Return a record with all the relevant information about the message currently being filtered.
on getMessageData()
	tell application "PowerMail"
		set theMessage to item 1 of (get current messages)
		
		set {subject:theSubject, time sent:timeSent, headers:theHeaders, content:bodyText} to theMessage
		set headerLines to theHeaders's paragraphs
		set theSender to my getHeaderValue(headerLines, "From") -- More informative than PowerMail's 'sender' property with redirected messages.
		set replyTo to my getHeaderValue(headerLines, "Reply-To")
		set messageID to my getMessageID(headerLines)
		set toRecipients to my recipientAddressDetails(theMessage's recipients whose recipient type is to recipient)
		set ccRecipients to my recipientAddressDetails(theMessage's recipients whose recipient type is cc recipient)
		set bccRecipients to my recipientAddressDetails(theMessage's recipients whose recipient type is bcc recipient)
		set theAttachments to my attachmentDetails(theMessage's attachments)
	end tell
	
	return {subject:theSubject, |time sent|:timeSent, sender:theSender, |reply to|:replyTo, |message ID|:messageID, |to recipients|:toRecipients, |cc recipients|:ccRecipients, |bcc recipients|:bccRecipients, attachments:theAttachments, content:bodyText}
end getMessageData

-- Get the value of a specified header from a list of the headers' lines.
on getHeaderValue(headerLines, theLabel)
	if (theLabel contains ":") then
		set theLabel to (text 1 thru (offset of ":" in theLabel) of theLabel) & space
	else
		set theLabel to theLabel & ": "
	end if
	set value to missing value
	set headerFound to false
	repeat with thisLine in headerLines
		if (thisLine begins with theLabel) then
			set value to text ((count theLabel) + 1) thru -1 of thisLine
			set headerFound to true
		else if (headerFound) then
			if ((thisLine begins with space) or (thisLine begins with tab)) then
				repeat
					set thisLine to text 2 thru -1 of thisLine
					if (not ((thisLine begins with space) or (thisLine begins with tab))) then exit repeat
				end repeat
				set value to value & space & text from word 1 to -1 of thisLine
			else
				exit repeat
			end if
		end if
	end repeat
	
	return value
end getHeaderValue

-- Derive a list of vanilla address-detail records from a list of PowerMail 'recipients'.
on recipientAddressDetails(theRecipients)
	tell application "PowerMail"
		repeat with thisRecipient in theRecipients
			set {address:{display name:displayName, email address:addr}} to thisRecipient
			set thisRecipient's contents to {|display name|:displayName, |email address|:addr}
		end repeat
	end tell
	
	return theRecipients -- The list has been reused for the records.
end recipientAddressDetails

-- Get the names, file sizes, and file aliases from a list of the message's 'attachments'.
on attachmentDetails(theAttachments)
	tell application "PowerMail"
		repeat with thisAttachment in theAttachments
			set {name:thisName, file size:thisSize, file:thisAlias} to thisAttachment
			set thisAttachment's contents to {|attachment name|:thisName, |attachment size|:thisSize, |attachment alias|:thisAlias}
		end repeat
	end tell
	
	return theAttachments -- The list has been reused for the result.
end attachmentDetails

-- Get the Message-ID to use as a unique file name. If none, make something up.
on getMessageID(headerLines)
	set messageID to getHeaderValue(headerLines, "Message-ID")
	
	-- If there's no Message-ID header, generate a random string from the current date, a random 8-digit number, and the current time.
	if (messageID is missing value) then
		set {year:y, month:m, day:d, hours:h, minutes:min, seconds:s} to (current date)
		set messageID to "<No_Message-ID_" & (y * 10000 + m * 100 + d) & (random number from 10000000 to 99999999) & text 2 thru -1 of ((1000000 + h * 10000 + min * 10 + s) as text) & ">"
	end if
	
	return messageID
end getMessageID

(* ******************** *)

(* The main part of the script processes the information returned. *)

-- The main process flow.
on process({subject:theSubject, |time sent|:timeSent, sender:theSender, |reply to|:replyTo, |message ID|:messageID, |to recipients|:toRecipients, |cc recipients|:ccRecipients, |bcc recipients|:bccRecipients, attachments:theAttachments, content:bodyText})
	
	-- If we're printing out the message, gather the bits of it into a list, interspersed with formatting codes for a pre-designed RTF document. We'll coerce the list to a single text, save the result as an .rtf file, and send it to the printer.
	if (prefPrintMessage) then
		-- Get a script object containing blocks of RTF formatting codes.
		set rtfStuff to getRTFStuff()
		
		considering case and diacriticals
			-- Start with data for a shaded rectangle containing the e-mail headers.
			if ((theSender is missing value) or ((count theSender) is 0)) then set theSender to "(unknown)"
			set collector to {rtfStuff's headersToSubject, "\\tab " & encode(theSubject), rtfStuff's dateHeader, "\\tab " & encode(timeSent as Unicode text), rtfStuff's fromHeader, "\\tab " & encode(undouble(TIDstuff(theSender, "\"", "")))}
			if (not ((replyTo is missing value) or ((count replyTo) is 0) or (theSender contains replyTo))) then
				set end of collector to rtfStuff's replyToHeader
				set end of collector to "\\tab " & encode(undouble(TIDstuff(replyTo, "\"", "")))
			end if
			if (toRecipients is not {}) then
				set end of collector to rtfStuff's toHeader
				set end of collector to getAddressBlock(toRecipients)
			end if
			if (ccRecipients is not {}) then
				set end of collector to rtfStuff's ccHeader
				set end of collector to getAddressBlock(ccRecipients)
			end if
			if (bccRecipients is not {}) then
				set end of collector to rtfStuff's bccHeader
				set end of collector to getAddressBlock(bccRecipients)
			end if
			set end of collector to rtfStuff's cellFoot
			-- Next an unshaded rectangle for the attachments (if any) and then the body text ” or vice versa, depending on the governing property preset.
			if (prefShowAttachmentsAtTop) then
				appendAttachments(theAttachments, rtfStuff, collector)
				appendBodyText(bodyText, rtfStuff, collector)
			else
				appendBodyText(bodyText, rtfStuff, collector)
				appendAttachments(theAttachments, rtfStuff, collector)
			end if
			-- Finish off with the RTF end bracket.
			set end of collector to rtfStuff's endBracket
			
			-- Coerce the list to a single text.
			set fullText to TIDstuff(collector, missing value, "")
		end considering
		
		-- Print it out.
		printMessage(fullText, messageID)
	end if
	
	-- If we're printing attachments (and there are any), do that.
	if (prefPrintAttachments) then printAttachments(theAttachments)
end process

-- Return a script object containing blocks of RTF formatting codes for a pre-designed document, inserting the relevant figures for US Letter or A4 page sizes.
on getRTFStuff()
	script rtfStuff
		property headersToSubject : {"{\\rtf1\\ansi\\ansicpg1252\\cocoartf1038\\cocoasubrtf360
{\\fonttbl\\f0\\fnil\\fcharset0 Verdana;\\f1\\fnil\\fcharset0 Geneva;}
{\\colortbl;\\red255\\green255\\blue255;\\red232\\green232\\blue232;\\red191\\green191\\blue191;}
", missing value, "\\viewkind1
\\deftab720

\\itap1\\trowd \\taflags0 \\trgaph108\\trleft-108 \\trbrdrt\\brdrnil \\trbrdrl\\brdrnil \\trbrdrt\\brdrnil \\trbrdrr\\brdrnil 
\\clvertalc \\clcbpat2 \\clwWidth", missing value, "\\clftsWidth3 \\clmart10 \\clmarl10 \\clmarb10 \\clmarr10 \\clbrdrt\\brdrs\\brdrw20\\brdrcf3 \\clbrdrl\\brdrs\\brdrw20\\brdrcf3 \\clbrdrb\\brdrs\\brdrw20\\brdrcf3 \\clbrdrr\\brdrs\\brdrw20\\brdrcf3 \\clpadt200 \\clpadl200 \\clpadb200 \\clpadr200 \\gaph\\cellx8640
\\pard\\intbl\\itap1\\tx1340\\pardeftab720\\ql\\qnatural

\\f0\\b\\fs24 \\cf0 Subject:
\\b0"}
		property USLetterDimensions : "\\margl1380\\margr1380\\vieww12240\\viewh15840"
		property USLetterCellWidth : "9000"
		property A4Dimensions : "\\paperw11900\\paperh16840\\margl1220\\margr1220\\vieww11900\\viewh16820"
		property A4CellWidth : "8980"
		property dateHeader : "\\

\\b Date:
\\b0"
		property fromHeader : "\\

\\b From:
\\b0"
		property replyToHeader : "\\

\\b Reply-To:
\\b0"
		property toHeader : "\\

\\b To:
\\b0"
		property ccHeader : "\\

\\b CC:
\\b0"
		property bccHeader : "\\

\\b BCC:
\\b0"
		property cellFoot : "\\cell \\lastrow\\row

"
		property attachmentCellHeader : {"

\\itap1\\trowd \\taflags0 \\trgaph108\\trleft-108 \\trbrdrt\\brdrnil \\trbrdrl\\brdrnil \\trbrdrt\\brdrnil \\trbrdrr\\brdrnil 
\\clvertalc \\clcbpat1 \\clwWidth", missing value, "\\clftsWidth3 \\clheight1040 \\clmart10 \\clmarl10 \\clmarb10 \\clmarr10 \\clbrdrt\\brdrs\\brdrw20\\brdrcf3 \\clbrdrl\\brdrs\\brdrw20\\brdrcf3 \\clbrdrb\\brdrs\\brdrw20\\brdrcf3 \\clbrdrr\\brdrs\\brdrw20\\brdrcf3 \\clpadt200 \\clpadl200 \\clpadb200 \\clpadr200 \\gaph\\cellx8640
\\pard\\intbl\\itap1\\tx1340\\pardeftab720\\ql\\qnatural

\\f0\\b \\cf0 "}
		property attachmentCellMiddle : " attachment(s):\\
\\pard\\intbl\\itap1\\tx360\\pardeftab720\\ql\\qnatural

\\b0 \\cf0 "
		property bodyHeader : "
\\pard\\pardeftab720\\ql\\qnatural\\pardirnatural

\\f1 \\cf0 \\
"
		property endBracket : "	}"
	end script
	
	if (prefPageSize is "A4") then
		set item 2 of rtfStuff's headersToSubject to rtfStuff's A4Dimensions
		set item 4 of rtfStuff's headersToSubject to rtfStuff's A4CellWidth
		set item 2 of rtfStuff's attachmentCellHeader to rtfStuff's A4CellWidth
	else if (prefPageSize is "US Letter") then
		set item 2 of rtfStuff's headersToSubject to rtfStuff's USLetterDimensions
		set item 4 of rtfStuff's headersToSubject to rtfStuff's USLetterCellWidth
		set item 2 of rtfStuff's attachmentCellHeader to rtfStuff's USLetterCellWidth
	else
		error
	end if
	
	return rtfStuff
end getRTFStuff

on undouble(addressLine)
	set o to (offset of "<" in addressLine)
	if ((o > 4) and (text 1 thru (o - 2) of addressLine is text (o + 1) thru ((offset of ">" in addressLine) - 1) of addressLine)) then
		set addressLine to text o thru -1 of addressLine
	else if (o is 0) then
		set addressLine to "<" & addressLine & ">"
	end if
	
	return addressLine
end undouble

-- Return a text block containing the sender details or those of a group of a particular recipient-type, with each party on its own line and preceded by a tab.
on getAddressBlock(addressDetails)
	if (addressDetails's class is record) then set addressDetails to {addressDetails}
	
	repeat with theseDetails in addressDetails
		set {|display name|:displayName, |email address|:emailAddress} to theseDetails
		if ((displayName's class is not in {text, Unicode text}) or ((count displayName) is 0) or (displayName is emailAddress)) then
			set theseDetails's contents to "<" & emailAddress & ">"
		else
			set theseDetails's contents to encode(displayName & (" <" & emailAddress & ">"))
		end if
	end repeat
	
	return ("\\tab " as Unicode text) & TIDstuff(addressDetails, missing value, "\\" & return & "\\tab ")
end getAddressBlock

-- Append the processed body text to the collector list.
on appendBodyText(bodyText, rtfStuff, collector)
	set end of collector to rtfStuff's bodyHeader
	set end of collector to TIDstuff(paragraphs of encode(bodyText), missing value, "\\" & return)
end appendBodyText

-- If there are any attachments, append a cell containing their names and sizes to the collector list.
on appendAttachments(theAttachments, rtfStuff, collector)
	if (theAttachments is not {}) then
		set end of collector to rtfStuff's attachmentCellHeader
		set end of collector to (count theAttachments)
		set end of collector to rtfStuff's attachmentCellMiddle
		set end of collector to getAttachmentBlock(theAttachments)
		set end of collector to rtfStuff's cellFoot
	end if
end appendAttachments

-- Return a text block containing attachment names, each on its own line and preceded by a tab and followed by a conveniently rounded size.
on getAttachmentBlock(attachmentDetails)
	set attachmentLines to {}
	repeat with theseDetails in attachmentDetails
		set {|attachment name|:thisName, |attachment size|:thisSize} to theseDetails
		set end of attachmentLines to encode(thisName) & " (" & convertByteSize(thisSize, prefKBSize, prefMaxDecPlacesInSizes) & ")"
	end repeat
	
	return ("\\tab " as Unicode text) & TIDstuff(attachmentLines, missing value, "\\" & return & "\\tab ")
end getAttachmentBlock

-- Convert a size in bytes to a convenient larger unit size with suffix. The 'KBSize' parameter specifies the number of units in the next unit up (1024 or 1000; or 'missing value' for 1000 in Snow Leopard or later and 1024 otherwise). The 'decPlaces' parameter specifies to how many decimal places the result is to be rounded (but not padded).
on convertByteSize(byteSize, KBSize, decPlaces)
	if (KBSize is missing value) then set KBSize to 1000 + 24 * (((system attribute "sysv") < 4192) as integer)
	
	if (byteSize is 1) then
		set conversion to "1 byte" as Unicode text
	else if (byteSize < KBSize) then
		set conversion to (byteSize as Unicode text) & " bytes"
	else
		set conversion to "Oooh lots!" -- Default in case yottabytes isn't enough!
		set suffixes to {" K", " MB", " GB", " TB", " PB", " EB", " ZB", " YB"}
		set dpShift to ((10 ^ 0.5) ^ 2) * (10 ^ (decPlaces - 1)) -- (10 ^ decPlaces) convolutedly to try to shake out any floating-point errors.
		repeat with p from 1 to (count suffixes)
			if (byteSize < (KBSize ^ (p + 1))) then
				tell ((byteSize / (KBSize ^ p)) * dpShift) to set conversion to (((it div 0.5 - it div 1) / dpShift) as Unicode text) & item p of suffixes
				exit repeat
			end if
		end repeat
	end if
	
	return conversion
end convertByteSize

-- Replace any special or non-ASCII characters in the text which are listed in this script's 'translationTable' property with the RTF equivalents provided.
on encode(txt)
	repeat with thisPair in translationTable
		set txt to TIDstuff(txt, (beginning of thisPair) as Unicode text, end of thisPair)
	end repeat
	
	return txt
end encode

-- Write the completed RTF text to a temporary file and send it to the printer.
on printMessage(fullText, messageID)
	-- Use a (hopefully) unique file name derived from the Message-ID or whatever the "application-specific" part of the script provides.
	set tempPath to (path to temporary items as Unicode text) & messageID & ".rtf"
	set fRef to (open for access file tempPath with write permission)
	try
		set eof fRef to 0
		write fullText as string to fRef
	end try
	close access fRef
	
	-- Size and margin parameters are needed for the printer because the RTF ones in the document aren't heeded.
	if (prefPageSize is "A4") then
		set options to "-o media=A4 -o page-left=61 -o page-right=61 -o page-top=72 -o page-bottom=72"
	else if (prefPageSize is "US Letter") then
		set options to "-o media=Letter -o page-left=69 -o page-right=69 -o page-top=72 -o page-bottom=72"
	end if
	if (prefPrintAgent is "lpr") then set options to options & " -r" -- Delete the file after printing it.
	
	tell application "TextEdit" to open file tempPath
	--printFile(tempPath as alias, options)
end printMessage

-- Send any attachment files to the print handler without further size or margin parameters. (I don't know if this is a good idea or not.)
on printAttachments(attachmentDetails)
	repeat with theseDetails in attachmentDetails
		printFile(theseDetails's |attachment alias|, "")
	end repeat
end printAttachments

-- Print a file, using either "lp" or "lpr" and a named or default printer, as specified in the script properties.
on printFile(theAlias, options)
	set quotedPOSIX to quoted form of POSIX path of theAlias
	if (prefPrinterName is not missing value) then
		if (prefPrintAgent is "lpr") then
			set destOption to "-P "
		else if (prefPrintAgent is "lp") then
			set destOption to "-d "
		end if
		set options to destOption & (quoted form of prefPrinterName) & space & options
	end if
	try
		do shell script (prefPrintAgent & space & options & space & (quoted form of POSIX path of theAlias))
	on error msg
		display dialog msg
	end try
end printFile

-- Get text items from text, coerce a list to (Unicode) text, or both.
on TIDstuff(object, cut, interpolate)
	set astid to AppleScript's text item delimiters
	if (cut is not missing value) then
		set AppleScript's text item delimiters to cut
		set object to object's text items
	end if
	if (interpolate is not missing value) then
		set AppleScript's text item delimiters to interpolate
		set object to object as Unicode text
	end if
	set AppleScript's text item delimiters to astid
	
	return object
end TIDstuff

Here’s a version for Mail of the application-specific code in the script above. It begins and ends with a row of commented asterisks and should replace the similarly delimited section in the script.

From the point of view of printing attachments, Mail has two problems. Firstly, it doesn’t automatically save attachments to their own files when they arrive and secondly, the ‘save’ command on which Martin’s script depends apparently doesn’t work in Lion.

I’ve noticed in Snow Leopard that moving or duplicating a message to another mailbox in Mail does cause any attachments to be saved to individual files at the destination. If this is true in Lion too (unfortunately my query about this last week in the OS X forum went unanswered), it can be used as a work-round for the non-functioning ‘save’ command. Set up a mailbox for the purpose in Mail and set the script property ‘prefAttachmentPrintingMailbox’ to the mailbox’s name (or the internal name/name/name path to it if its nested inside another mailbox). If the script property ‘prefPrintAttachments’ is set to true, any messages with attachments will be duplicated to the special mailbox and its attachments printed from there.

It takes a while for the duplication to be effected and the folder will need purging occasionally, but the idea works. Hopefully the work-round won’t be needed for long.

(* ******************** *)

(* This section should be written for your e-mail client and the way its mail actions run AppleScripts. (The code here is for Apple's Mail.) It should get all the relevant data from the message and pass them to the process() handler in the form of a vanilla user record:

{subject: <The message's subject line (text)>, ¬
|time sent|: <The date/time sent (AppleScript date object)>, ¬
sender: <The sender's details from the e-mail's "From:" header (text, eg. "\"Joe Bloggs\" <joe@bloggs.com>")>, ¬
|reply to|: <The Reply-To address, if any (ditto)>, ¬
|to recipients|: <Details of 'to' recipients (list of records, eg. {{|display name|:"Fred Bloggs", |email address|:"fred@bloggs.com"}, {|display name|:"Sid Bloggs", |email address|:"loretta@bloggs.com"}})>, ¬
|cc recipients|: <Details of any 'CC' recipients (list of records ditto)>, ¬
|bcc recipients|: <Details of any 'BCC' recipients (list of records ditto)>, ¬
attachments: <Details of any attachments (list of records, eg. {{|attachment name|:"My photo.jpg", |attachment size|:23527, |attachment alias|:alias "Path:To:attachments folder:My photo.jpg"}})>, ¬
content: <The message's body text>}
*)

-- The name of a dedicated mailbox in Mail.
property prefAttachmentPrintingMailbox : "Attachment printing" -- A "root" mailbox called "Attachment printing".

-- Run handler for testing.
on run
	tell application "Mail"
		get item 1 of (get selection)
		my getMessageData(result)
		my process(result)
	end tell
end run

-- Mail's "Rules" run AppleScripts through special handlers.
using terms from application "Mail"
	on perform mail action with messages matchmsgs for rule therule
		repeat with matchmsg in matchmsgs
			my getMessageData(matchmsg)
			my process(result)
		end repeat
	end perform mail action with messages
end using terms from

-- Return a record with all the relevant information about the message currently being filtered.
on getMessageData(theMessage)
	tell application "Mail"
		set {subject:theSubject, date sent:dateSent, reply to:replyTo, message id:messageID, content:bodyText} to theMessage
		set theSender to my getHeaderValue(theMessage, "From") -- I prefer the header to Mail's 'sender' value with redirected messages.
		set messageID to my getMessageID(messageID)
		set toRecipients to my recipientAddressDetails(theMessage's to recipients)
		set ccRecipients to my recipientAddressDetails(theMessage's cc recipients)
		set bccRecipients to my recipientAddressDetails(theMessage's bcc recipients)
		set theAttachments to my attachmentDetails(theMessage)
	end tell
	
	return {subject:theSubject, |time sent|:dateSent, sender:theSender, |reply to|:replyTo, |message ID|:messageID, |to recipients|:toRecipients, |cc recipients|:ccRecipients, |bcc recipients|:bccRecipients, attachments:theAttachments, content:bodyText}
end getMessageData

-- Get the value of a specified header.
on getHeaderValue(theMessage, theLabel)
	tell application "Mail"
		tell (theMessage's header theLabel)
			if (it exists) then
				set theSender to its content
			else
				set theSender to missing value
			end if
		end tell
	end tell
	
	return theSender
end getHeaderValue

-- Derive a list of vanilla address-detail records from a list of Mail 'recipients'.
on recipientAddressDetails(theRecipients)
	tell application "Mail"
		repeat with thisRecipient in theRecipients
			set {name:displayName, address:emailAddress} to thisRecipient's properties -- 'properties' forces a 'missing value' when the 'name' doesn't exist.
			set thisRecipient's contents to {|display name|:displayName, |email address|:emailAddress}
		end repeat
	end tell
	
	return theRecipients -- The list has been reused for the records.
end recipientAddressDetails

-- Get the names, file sizes, and file aliases of the message's 'attachments'.
on attachmentDetails(theMessage)
	tell application "Mail" to set theAttachments to theMessage's mail attachments
	
	if ((prefPrintAttachments) and (theAttachments is not {})) then
		tell application "Mail"
			set allHeaders to theMessage's all headers
			duplicate theMessage to mailbox prefAttachmentPrintingMailbox
			repeat until ((first message of mailbox prefAttachmentPrintingMailbox whose all headers is allHeaders) exists)
				delay 0.2
			end repeat
			set theMessage to first message of mailbox prefAttachmentPrintingMailbox whose all headers is allHeaders
			set idMsg to (theMessage's id as Unicode text) & ":"
			
			set attachmentCount to (count theAttachments)
			repeat until (mail attachments 1 thru attachmentCount of theMessage exists)
				delay 0.2
			end repeat
			set theAttachments to theMessage's mail attachments
		end tell
		set attachmentFolderPath to ((POSIX path of (path to library folder from user domain as Unicode text)) & "Mail/Mailboxes/" & prefAttachmentPrintingMailbox & ".mbox/Attachments/") as POSIX file as text
	end if
	
	repeat with thisAttachment in theAttachments
		tell application "Mail" to set {name:thisName, file size:thisSize, id:idAtt} to thisAttachment
		if (prefPrintAttachments) then
			set attachmentPath to attachmentFolderPath & (idMsg & idAtt & ":" & thisName)
			repeat
				try
					set attachmentAlias to attachmentPath as alias
					exit repeat
				on error
					delay 0.2
				end try
			end repeat
		else
			set attachmentAlias to missing value
		end if
		set thisAttachment's contents to {|attachment name|:thisName, |attachment size|:thisSize, |attachment alias|:attachmentAlias}
	end repeat
	
	return theAttachments -- The list has been reused for the result.
end attachmentDetails

-- If the message has no Message-ID, make up something to use as a unique file name.
on getMessageID(messageID)
	if ((count messageID) is 0) then
		-- If there's no Message-ID header, generate a random string from the current date, a random 8-digit number, and the current time.
		set {year:y, month:m, day:d, hours:h, minutes:min, seconds:s} to (current date)
		set messageID to "<No_Message-ID_" & (y * 10000 + m * 100 + d) & (random number from 10000000 to 99999999) & text 2 thru -1 of ((1000000 + h * 10000 + min * 10 + s) as text) & ">"
	end if
	
	return messageID
end getMessageID

(* ******************** *)