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: 2022-12-27

Description

Please see the Setting Up a Spam Filtering Drone section of the manual for more information about what this script does. If you’re using the standard setup where spam messages are moved to the Junk mailbox, you can simply click here to download the script. If you’re using an older setup with the Spam mailbox, 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 macOS 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 pColorSpamMessages : true
property pFlagSpamMessages : false
property pMarkGoodMessagesUnread : false
property pMoveBySettingMailbox : true
property pSpamMailboxName : "Spam"
property pEnableDebugLogging : false
global useJunkMailbox

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"}, {""})
    
return _host
end hostNameForDrone

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

on logToConsole(_message)
    
set _logMessage to "SpamSieve [Apple Mail Remote Training MJTLog] " & _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()
    
if application "Mail" is not running then return
    
if my shouldDisableOnThisMac() then return
    
tell application "SpamSieve"
        
set useJunkMailbox to lookup single key "AppleMailUseJunkMailbox" without default value
    
end tell
    
try
        
tell application "Mail" to get version
    
on error _error number _errorNumber
        
if _errorNumber is -1743 then -- errAEEventNotPermitted
            
set _alertMessage to "You can give “Apple Mail - Remote Training” access to control Mail and SpamSieve from System Preferences > Security & Privacy > Privacy > Automation. For more information, please see:

https://c-command.com/spamsieve/help/security-privacy-acce"
            
display alert _error message _alertMessage
        
end if
    
end try
    
tell application "Mail"
        
set _accounts to my accountsToCheck()
        
repeat with _account in _accounts
            
my debugLog("Checking account: " & _account's name)
            
try
                
my trainMessagesInAccount(_account)
            
on error _error
                
my logToConsole("Error training from account “" & _account's name & "”: " & _error)
            
end try
        
end repeat
    
end tell
end doRemoteTraining

on accountsToCheck()
    
tell application "Mail"
        
set _result to {}
        
if useJunkMailbox then
            
repeat with _account in accounts
                
try
                    
if _account's enabled then
                        
my findMailbox("TrainSpam", _account)
                        
copy _account to end of _result
                    
end if
                
on error
                    
-- Avoid creating a mailbox
                
end try
            
end repeat
        
else
            
repeat with _accountName in my accountNamesForDrone()
                
set _account to account _accountName
                
copy _account to end of _result
            
end repeat
        
end if
        
return _result
    
end tell
end accountsToCheck

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 pColorSpamMessages then
                
set _message's background color to blue
            
end if
            
if pFlagSpamMessages then
                
set _message's flag index to 6
            
end if
            
if pMarkSpamMessagesRead then
                
set _message's read status to true
            
end if
            
my moveMessage(_message, my spamMailboxForAccount(_account), true)
        
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 pColorSpamMessages then
                
set _message's background color to none
            
end if
            
if pFlagSpamMessages then
                
set _message's flag index to -1
            
end if
            
if pMarkGoodMessagesUnread then
                
set _message's read status to false
            
end if
            
my moveMessage(_message, my inboxForAccount(_account), false)
        
end repeat
    
end tell
end trainMessagesInAccount

on messagesFromMailbox(_mailboxName, _account)
    
tell application "Mail"
        
set _accountName to name of _account
        
try
            
set _mailbox to my findMailbox(_mailboxName, _account)
        
on error
            
tell _account
                
make new mailbox with properties {name:_mailboxName}
            
end tell
            
set _mailbox to mailbox _mailboxName of _account
        
end try
        
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 findMailbox(_mailboxName, _account)
    
tell application "Mail"
        
try
            
return mailbox _mailboxName of _account
        
on error
            
-- my debugLog("Looking for nested mailbox " & _mailboxName & " in account " & _account's name)
            
set _result to my findNestedMailbox(_mailboxName, mailboxes of _account)
            
if _result is not missing value then
                
my debugLog("Found nested mailbox " & _mailboxName & " in account " & _account's name)
                
return _result
            
end if
            
my debugLog("No nested mailbox " & _mailboxName & " in account " & _account's name)
            
error "No mailbox named " & _mailboxName
        
end try
    
end tell
end findMailbox

on findNestedMailbox(_mailboxName, _mailboxes)
    
tell application "Mail"
        
repeat with _mailbox in _mailboxes
            
if name of _mailbox is _mailboxName then
                
return _mailbox
            
end if
        
end repeat
        
repeat with _mailbox in _mailboxes
            
set _result to my findNestedMailbox(_mailboxName, mailboxes of _mailbox)
            
if _result is not missing value then
                
return _result
            
end if
        
end repeat
        
return missing value
    
end tell
end findNestedMailbox

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, _isSpam)
    
tell application "Mail"
        
if not useJunkMailbox then
            
my debugLog(my makeLogMessage("Moving message to", _mailbox, _message's subject))
            
set _message's mailbox to _mailbox
            
return
        
end if
        
if _isSpam then
            
my debugLog("Moving message to Junk: " & _message's subject)
            
move _message to junk mailbox
        
else
            
if pMoveBySettingMailbox then
                
my debugLog(my makeLogMessage("Moving message to", _mailbox, _message's subject))
                
set _message's mailbox to _mailbox
            
else
                
-- Not used by default because for many users copies rather than moves messages that Mail reports are in Gmail's All Mail, however some users report that this works better (#kZMU4ZD8QOzpruuManwLChA).
                
my debugLog("Moving message to Inbox: " & _message's subject)
                
move _message to inbox
            
end if
        
end if
    
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)
    
if useJunkMailbox then return missing value
    
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