This script works around a bug in Mail.app’s AppleScript.
It can be used to only import specific attachments. If you e.g. don’t want to import images filter them out and import everything else.
As it’s not possible to use mail attachment's property MIME type (which throws an error) it’s necessary to use AppleScriptObjC to work around.
Note:
This script is a working demo that shows how to import specific attachments to the global inbox.
It is not a full blown attachment import script.
What you want are the lines that are commented with this line is necessary to get the MIME type and the handlers.
-- Import mail attachments filtered by MIME type
use AppleScript version "2.4"
use framework "Foundation"
use scripting additions
tell application "Mail"
try
set theSelection to selection
if theSelection = {} then error "Please select some mail"
set {theTempDirectoryURL, theTempDirectoryPath} to my createTempDirectory() -- this line is necessary to get the MIME type
set theMessageIDs_processed to {}
repeat with thisMessage in theSelection
set thisMessage_ID to message id of thisMessage
if thisMessage_ID is not in theMessageIDs_processed then -- necessary if used with Smart Mailboxes, without it non-zip attachments are processed twice
set theAttachments to mail attachments of thisMessage
repeat with thisAttachment in theAttachments
if (downloaded of thisAttachment) then
set {thisAttachment_MIMEType, thisAttachment_TempPath} to my getMIMEType(thisAttachment, theTempDirectoryPath) -- this line is necessary to get the MIME type
-- do something with the MIME type , e.g. …
if thisAttachment_MIMEType is not in {"image/gif", "image/jpeg", "image/png", "image/tiff"} then
if thisAttachment_MIMEType ≠ "application/zip" then
set thisPath to thisAttachment_TempPath
else
set thisPath to theTempDirectoryPath & "/" & (my getGloballyUniqueString())
do shell script "unzip " & quoted form of thisAttachment_TempPath & " -x __MACOSX/* -d " & quoted form of thisPath
end if
set thisAttachment_Name to name of thisAttachment
tell application id "DNtp" to import thisPath name thisAttachment_Name to incoming group
end if
end if
end repeat
set end of theMessageIDs_processed to thisMessage_ID
end if
end repeat
my deleteTempDirectory(theTempDirectoryURL) -- this line is necessary to get the MIME type
on error error_message number error_number
if the error_number is not -128 then
activate
display alert "Mail" message error_message as warning
my deleteTempDirectory(theTempDirectoryURL)
end if
return
end try
end tell
on createTempDirectory()
try
set theTempDirectoryPath to (current application's NSTemporaryDirectory())'s stringByAppendingString:("com.apple.mail/TemporaryItems/Mail MIME Type" & space & (current application's NSProcessInfo's processInfo()'s globallyUniqueString()))
set theTempDirectoryURL to current application's |NSURL|'s fileURLWithPath:theTempDirectoryPath
set theFileManager to current application's NSFileManager's defaultManager()
set {successCreateDir, theError} to theFileManager's createDirectoryAtURL:theTempDirectoryURL withIntermediateDirectories:false attributes:(missing value) |error|:(reference)
if theError ≠ missing value then error (theError's localizedDescription() as string)
return {theTempDirectoryURL, theTempDirectoryPath as string}
on error error_message number error_number
activate
if the error_number is not -128 then display alert "Error: Handler \"createTempDirectory\"" message error_message as warning
error number -128
end try
end createTempDirectory
on getMIMEType(theAttachment, theTempDirectoryPath)
try
tell application "Mail"
try
set theAttachment_TempPath to theTempDirectoryPath & "/" & (name of theAttachment)
save theAttachment in theAttachment_TempPath as native format
on error error_message number error_number
if the error_number is not -128 then display alert "Mail" message error_message as warning
end try
end tell
set theURLRequest to current application's NSURLRequest's requestWithURL:(current application's |NSURL|'s fileURLWithPath:theAttachment_TempPath)
set {theURLData, theURLResponse, theError} to current application's NSURLConnection's sendSynchronousRequest:theURLRequest returningResponse:(reference) |error|:(reference)
if theError ≠ missing value then error (theError's localizedDescription() as string)
set theAttachment_MIMEType to (theURLResponse's MIMEType()) as string
return {theAttachment_MIMEType, theAttachment_TempPath}
on error error_message number error_number
activate
if the error_number is not -128 then display alert "Error: Handler \"getMIMEType\"" message error_message as warning
error number -128
end try
end getMIMEType
on deleteTempDirectory(theTempDirectoryURL)
try
set {successDeleteDir, theError} to (current application's NSFileManager's defaultManager()'s removeItemAtURL:(theTempDirectoryURL) |error|:(reference))
if theError ≠ missing value then error (theError's localizedDescription() as string)
on error error_message number error_number
activate
if the error_number is not -128 then display alert "Error: Handler \"deleteTempDirectory\"" message error_message as warning
error number -128
end try
end deleteTempDirectory
on getGloballyUniqueString()
try
return (current application's NSProcessInfo's processInfo()'s globallyUniqueString()) as string
on error error_message number error_number
activate
if the error_number is not -128 then display alert "Error: Handler \"getGloballyUniqueString\"" message error_message as warning
error number -128
end try
end getGloballyUniqueString
Impressive, as usual. I’d suggest to use macOS’s file command in getMIMEType, though. That should reduce this subroutine to
on getMIMEType(theAttachment, theTempDirectoryPath)
try
tell application "Mail"
try
set theAttachment_TempPath to theTempDirectoryPath & "/" & (name of theAttachment)
save theAttachment in theAttachment_TempPath as native format
on error error_message number error_number
if the error_number is not -128 then display alert "Mail" message error_message as warning
end try
end tell
set theAttachment_MIMEtype to do shell script "file --mime-type '" & theAttachemnt_TempPath & "'"
return {theAttachment_MIMEType, theAttachment_TempPath}
on error error_message number error_number
activate
if the error_number is not -128 then display alert "Error: Handler \"getMIMEType\"" message error_message as warning
error number -128
end try
end getMIMEType
I’m not at all sure about the quoting for do shell command, though. What I tried to achieve was something like file --mime-type 'filename'
so that the filename is included in single quotes, which allows for blanks etc. in filenames.
On the one hand, using file unfortunately removes all these lovely, longwinded calls to NS… routines. On the other hand it makes everything a bit cleaner (and perhaps faster, but who knows).
Another take on this task, using JavaScript, and working only for PDFs and images (see desiredAttachments[] at the top).
The script as it stands now does not import anything into DT, it just writes the attachments to the folder ~/Desktop/Attachments (see targetfolder at the top).
Edit: This is the third version of the script. In the second one, I tried to avoid using Mail’s save method by decoding the attachment’s source with a shell command. This did not work reliably, since the source data can exceed the maximum command line length of the shell.
As I didn’t comment the script very sparingly, here’s what it does:
for all currently selected e-mails
get the source of the e-mail
find all “boundary” definitions. Those are used to separate the different parts of an MIME e-mail
build a regular expression of the form “^–(boundary1|boundary2|…)$”
use this regular expression to split the mail source in its MIME parts
for every MIME part
find the Content-Disposition header.
if it exists and is either “attachment” or “inline”
get the values for the headers “Content-Type” and “name” and save them in an array of objects (partInfo[])
extract all elements of the latter array into a new array attachments with a filter call that accepts only those MIME types defined in desiredAttachments (see top). A regular expression is used for that.
For every element of this array (i.e. every desired attachment)
get the corresponding mailAttachment element from the message using the name. this works because the Mail app uses the name of an attachment in the e-mail again to name it internally.
save this attachment to the targetFolder.
(() => {
function writeToFile(name, msg, folder) {
const attachment = msg.mailAttachments[name];
Mail.save(attachment, {in: Path(`${folder}/${name}`)});
}
const curApp = Application.currentApplication();
curApp.includeStandardAdditions = true;
const targetFolder = `${curApp.pathTo("desktop")}/Attachments`;
const desiredAttachments = ['application/pdf', 'image/*'];
const Mail = Application("Mail");
const selMsg = (Mail.messageViewers[0]).selectedMessages();
selMsg.forEach( m => {
const src = m.source();
/* Find all boundary definitions in the mail source */
const boundaries = [... src.matchAll(/boundary="(.*?)"/g)];
/* Build a regular expression of the form /^--(boundary1|boundary2|...$/ */
const splitRE = new RegExp(`^--(${boundaries.map(b => b[1]).join('|')})$`,"m");
/* Split the mail source in parts at the boundaries */
const msgParts = src.split(splitRE);
const partInfo = []; // Array to store attachment info
/* For every part of the message … */
msgParts.forEach(part => {
const disposition = part.match(/Content-Disposition:\s+(.*?);/);
/* … if it is an attachment or an inline element: get its MIME type, name and Base64 data */
if (disposition && (disposition[1] === "attachment" || disposition[1] === "inline")) {
const type = part.match(/Content-Type:\s+(.*?);/);
const name = part.match(/name="(.*?)"/);
/* Safe MIME type and name only if both are defined */
if (type && name) {
partInfo.push({
"type": type[1],
"name": name[1],
});
}
}
})
const attachmentRE = new RegExp(desiredAttachments.join('|'));
const attachments = partInfo.filter(p => attachmentRE.test(p.type))
/* Lazy way to create a new folder, no error if it exists already */
curApp.doShellScript(`mkdir -p "${targetFolder}"`);
/* Write attachments to target folder. Note that existing files will be silently overwritten! */
attachments.forEach( a => {
const fileName = `${targetFolder}/${a.name}`;
writeToFile(a.name, m, targetFolder);
})
})
})()
I actually didn’t check whether it would be possible via do shell script because calling anything via do shell script is quite slow. It’s not bad if it’s called few times in a script but as soon as you use it in a repeat it really adds up, so I avoid it if possible (and writing my own makes sense and I can do it). However in this case there’s no real speed difference.
To escape strings use quoted form of . I’ve also added the -b flag
set theAttachment_MIMEtype to do shell script "file -b --mime-type " & quoted form of theAttachment_TempPath
How exactly do you plan to use the MIME type in your script? In a mail rule? In my tests I excluded images which then imported 100 attachments instead of around 400 without filtering. But I’m actually not sure where to use it.
Frankly: I do not plan to use it at all There was a question here some time ago about “saving only PDF attachements to DT” . I found that an interesting challenge to implement in JavaScript (and of course I found the AS solution clumsy ), so I tried to do that. Which landed me smack in this stupid scripting bug of Apple’s (not the only one, btw).
Now there seems to be something working. I’m not quite happy about the “save to folder, then import to DT” part in both our scripts. It would nicer, if one could just use the mail source, decode that and pipe it directly into a createRecordWith call for DT. No success with that so far, though.