Script: Import mail attachments filtered by MIME type

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.

Nice! I’ll try that in AppleScript

Frankly: I do not plan to use it at all :wink: 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 :stuck_out_tongue_winking_eye:), 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.

Spoil yourself.