Apple Mail - Remote Training

Summary: Training script for setting up a spam filtering drone with Apple Mail.
Requires: SpamSieve, Apple Mail
Install Location: ~/Library/Application Scripts/ or ~/Library/Scripts/Applications/Mail/
Last Modified: 2016-11-18


Please see the Setting Up a Spam Filtering Drone section of the manual. You’ll need to enter the names of the IMAP and Exchange accounts that you want the drone to operate on. For example, if you have one account named “Bob iCloud” the modified part of the script would be:

on accountNamesForDrone()
    return {"Bob iCloud"}
end accountNamesForDrone

For two accounts, you would instead enter something like:

on accountNamesForDrone()
    return {"Bob iCloud", "Bob Gmail"}
end accountNamesForDrone

To test this script you can run it in Script Editor (a.k.a. AppleScript Editor) and look for any error messages in the Console application. You can also enable debug logging by changing pEnableDebugLogging from false to true.

Normally, the script will run when Mail receives a new message in the inbox and applies the “Remote Training” rule. You can also set up the script as a standalone application. This has less overhead than running the script from within Mail, and it allows the remote training to happen on a more predictable schedule. To do this, use Script Editor to save the script as an application (with the Stay Open option checked) and then launch the application. You can set in the “idle” handler how often the remote training should occur.

The first time you run the script, it may take a long time if there are lots of messages in the training mailboxes. It will wait for three minutes for Mail to fetch the messages before giving up. You can speed this up by manually training those messages.

The script may also take a long time to run if the training mailboxes contain lots of messages that Mail has deleted but not yet purged. You can speed this by up deleting and recreating the training mailboxes to get rid of those messages.

Note: On Mac OS X 10.8 or later, the script must be saved in the folder /Users/<username>/Library/Application Scripts/ This is because Mail is sandboxed and only has access to run scripts in that folder. To access the Library folder, click on the Go menu in the Finder while holding down the Option key.

Installation Instructions · Download in Compiled Format · Download in Text Format


property pMarkSpamMessagesRead : false
property pMarkGoodMessagesUnread : false
property pSpamMailboxName : "Spam"
property pEnableDebugLogging : false

on accountNamesForDrone()
-- Enter your account names here. If you have more than one, separate with commas: {"Account 1", "Account 2"}
-- The account name comes from the "Description" field in the Accounts tab of Mail's preferences.
return {"Account 1"}
end accountNamesForDrone

on spamMailboxNamesByAccount()
-- You can specify pairs here, e.g. {{"Work Account", "Junk"}, {"Personal Account", "Spam"}}
-- to have different spam mailbox names for each account. If an account is not specified,
-- it defaults to pSpamMailboxName.
return {}
end spamMailboxNamesByAccount

on hostNameForDrone()
return ""
-- Remove the above line if iCloud rule syncing is working incorrectly,
-- so that your remote training rule cannot be made inactive on the
-- non-drone Macs.
set {_host} to my lookupDefaults({"AppleMailEnabledHostName"}, {""})
end hostNameForDrone

on debugLog(_message)
if pEnableDebugLogging then my logToConsole(_message)
end debugLog

on logToConsole(_message)
set _logMessage to "SpamSieve [Apple Mail Remote Training] " & _message
do shell script "/usr/bin/logger -s " & _logMessage's quoted form
end logToConsole

on run
-- This is executed when you run the script directly.
my doRemoteTraining()
end run

on idle
-- This is executed periodically when the script is run as a stay-open application.
my doRemoteTraining()
return 60 * 5 -- Run again in 5 minutes.
end idle

on shouldDisableOnThisMac()
set _droneHost to my hostNameForDrone()
if _droneHost is "" then return false
set _currentHost to do shell script "/usr/bin/uname -n"
if _droneHost is _currentHost then return false
my debugLog("Drone disabled on host: " & _currentHost)
return true
end shouldDisableOnThisMac

using terms from application "Mail"
on perform mail action with messages _messages
-- This is executed when Mail runs the rule.
my doRemoteTraining()
end perform mail action with messages
end using terms from

on doRemoteTraining()
tell application "System Events"
if not (exists process "Mail") then return
end tell
if my shouldDisableOnThisMac() then return
tell application "Mail"
repeat with _accountName in my accountNamesForDrone()
my debugLog("Checking account: " & _accountName)
set _account to account _accountName
my trainMessagesInAccount(_account)
on error _error
my logToConsole("Error training from account “" & _accountName & "”: " & _error)
end try
end repeat
end tell
end doRemoteTraining

on trainMessagesInAccount(_account)
tell application "Mail"
set _messages to my messagesFromMailbox("TrainSpam", _account)
repeat with _message in _messages
set _source to my sourceFromMessage(_message)
tell application "SpamSieve" to add spam message _source
set _message's junk mail status to true
if pMarkSpamMessagesRead then
set _message's read status to true
end if
my moveMessage(_message, my spamMailboxForAccount(_account))
end repeat
set _messages to my messagesFromMailbox("TrainGood", _account)
repeat with _message in _messages
set _source to my sourceFromMessage(_message)
tell application "SpamSieve" to add good message _source
set _message's junk mail status to false
if pMarkGoodMessagesUnread then
set _message's read status to false
end if
my moveMessage(_message, my inboxForAccount(_account))
end repeat
end tell
end trainMessagesInAccount

on messagesFromMailbox(_mailboxName, _account)
tell application "Mail"
set _accountName to name of _account
set _mailbox to mailbox _mailboxName of _account
my debugLog(my makeLogMessage("Getting messages in mailbox", _mailbox, "This can take a long time if there are many messages."))
with timeout of 3 * 60 seconds
set _messages to messages of _mailbox whose deleted status is false
end timeout
my debugLog(my makeLogMessage("Messages in mailbox", _mailbox, count of _messages))
return _messages
end tell
end messagesFromMailbox

on sourceFromMessage(_message)
tell application "Mail"
my debugLog(my makeLogMessage("Getting source of message in", _message's mailbox, _message's subject))
return _message's source
end tell
end sourceFromMessage

on moveMessage(_message, _mailbox)
tell application "Mail"
my debugLog(my makeLogMessage("Moving message to", _mailbox, _message's subject))
set _message's mailbox to _mailbox
end tell
end moveMessage

on makeLogMessage(_action, _mailbox, _detail)
return _action & " " & my describeMailbox(_mailbox) & ": " & _detail
end makeLogMessage

on describeMailbox(_mailbox)
tell application "Mail"
set _mailboxName to _mailbox's name
set _accountName to name of _mailbox's account
on error
set _accountName to "On My Mac"
end try
return "“" & _accountName & "” / “" & _mailboxName & "”"
end tell
end describeMailbox

on spamMailboxForAccount(_account)
tell application "Mail"
repeat with _pair in my spamMailboxNamesByAccount()
if item 1 of _pair is name of _account then
set _name to item 2 of _pair
return mailbox _name of _account
end if
end repeat
return mailbox pSpamMailboxName of _account
end tell
end spamMailboxForAccount

on inboxForAccount(_account)
tell application "Mail"
set _names to {"INBOX", "Inbox", "innboks", "Posteingang", "Boite de reception"}
repeat with _name in _names
set _mailbox to mailbox _name of _account
return _mailbox
end try
end repeat
return inbox
end tell
end inboxForAccount

on lookupDefaults(_keys, _defaultValues)
tell application "SpamSieve"
set _result to {}
repeat with _i from 1 to count of _keys
set _key to item _i of _keys
set _defaultValue to item _i of _defaultValues
set _value to lookup single key _key default value _defaultValue
copy _value to end of _result
end repeat
return _result
on error -- SpamSieve 2.9.15 and earlier
return lookup keys _keys default values _defaultValues
end try
end tell
end lookupDefaults