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 thesave
method is not called at themailAttachment
but at thesave thy self
might be more usual today. Butsave()
(as some other methods likemake()
andopen()
) are one of the building blocks of Apple’s scripting architecture and as such inherited by everyApplication
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);
})
})
})()