Mail

Automating Mail

Apple’s Mail program is not necessarily one of the best examples to use scripting with. Trying to access certain properties, notably the size and MIME type of attachments, always raises an error – since 2015. So if you want to save attachments from the selected e-mails, you have to take another approach.

Sending an e-mail#

Sending an e-mail with Apple’s Mail program is interesting because it shows how to build a new object in JXA. Apple’s standard suite offers the make method for that. However, there’s a simpler way, especially if you have to pass attributes to the new object.

You can simply call a method with the same name as the class of the object you want to create, passing it the attributes and their values as an object. For a new message, this looks like mail.OutgoingMessage({sender: "me@…", subject: "Test"}), with sender (obviously) being the sender of the e-mail and subject its subject.

A very simple script to send mail to someone is shown below.

(() => {
  const app = Application("Mail");
  const msg = app.OutgoingMessage({sender: "me@example.com",
           subject: "Test"});
  app.outgoingMessages.push(msg);
  const rcpt = app.Recipient({name: "Your Name", 
    address: "you@example.com"});
  msg.toRecipients.push(rcpt);
  msg.content = "Hi there";
  app.activate();
  msg.send();
})()

The script uses the same method just shown to create a recipient, too. Both the message and the recipient are pushed onto arrays: one for the outgoing messages and one containing all the recipients of the message. Given the inner workings of JXA, you can’t assign an array to these lists like so app.outgoingMessages = [msg].

This is also important if you want to add attachments to your message. You would first create a new Attachment object with const att = app.Attachment({filename: "/Users/you/path/to/file"}) and then push it onto the Attachments array with msg.attachments.push(att). Take care of the upper/lower case spelling: The object’s name begins with an uppercase letter, the array name with a lowercase letter.

Saving (some) attachments#

You can of course save e-mail attachments by right clicking on them or using “Save attachments…” from the “File” menu. But doing that for a lot of e-mails is tedious, so scripting can help. The following example works through all selected messages and saves those attachments whose MIME type matches one of a pre-defined list (desiredAttachments[]).

This list accepts complete MIME types like “application/pdf” and regular expressions like “image/.*”. It compares the attachment’s MIME type against all elements in this list and saves only those where the type matches.

The folder to save the attachments to is defined in the variable targetFolder as the “Attachment” folder on the users’s desktop. It is created with a shell command (see bottom of the script) that does not raise an error if the folder already exists.

In order to determine an attachment’s MIME type and name, the script inspects the message source, i.e. the raw data. A MIME message is separated by special boundary lines whose content is defined in the message header by a line like “boundary=xxxx”.

So a MIME message looks like (very simplified)

Message headers
    boundary="xxxx"
--xxxx
      Content-type: "MIME type"
      [Content-Disposition: "inline"|"attachment"]
      [name="something"]
      Base64 encoded data for this part
--xxxx
      Content-type: "MIME type"
      [Content-Disposition: "inline"|"attachment"]
      [name="something"]
      Base64 encoded data for this part
--xxxx
…

The script uses JavaScript’s split() method and the boundaries it fished out of the source beforehand to break up the original message into its different parts.

But only some of these parts are attachments, namely those with a “Content-Disposition:” header whose value is “attachment”. When looping over all message parts, the script saves only the names and MIME types of the parts with the correct disposition setting to the array partInfo.

Finally, it goes over all attachments for the current message in partInfo[] and filters out those whose MIME type matches one of the desired ones using a Regular Expression. Finally, the function writeToFile uses Mail’s save method to write the attachment to disk.

This part relies on the fact that an attachment’s name in Mail app is identical to its name in the message source: const attachment = msg.mailAttachment[name]; gets the correct attachment as an Object Specifier which can then be passed to save().

Note that the save method is not called at the mailAttachment but at the Mail application itself. This looks a bit weird, something like save thy self might be more usual today. But save() (as some other methods like make() and open()) are one of the building blocks of Apple’s scripting architecture and as such inherited by every Application object.
(() => {
  
  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 desiredTypes = ['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: get its MIME type and name */
      if (disposition && (disposition[1] === "attachment")) {
        
        const type = part.match(/Content-Type:\s+(.*?);/);
        const name = part.match(/name="(.*?)"/);
        
        /* Safe MIME type and name in partInfo 
         * only if both are defined 
         */
        if (type && name) {
          partInfo.push({
            "type": type[1],
            "name": name[1],
          });
        }
      }
    })
    const attachmentRE = new RegExp(desiredTypes.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);
    })
  })
})()