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/com.apple.mail or ~/Library/Scripts/Applications/Mail/
Last Modified: 2017-03-03

Description

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/com.apple.mail. 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

Script

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)
            
try
                
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
        
try
            
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
            
try
                
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"
        
try
            
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