Outlook - Filter Mailboxes

Summary: Periodically filter new messages in the inbox with SpamSieve.
Requires: SpamSieve, Outlook 365
Install Location: /Applications
Last Modified: 2022-06-10

Description

This script application is to help SpamSieve to automatically filter new messages in Outlook 2016 (also known as Outlook 365), since Outlook 2016 currently does not have the built-in ability to automatically apply AppleScripts (such as SpamSieve) to incoming messages.

For instructions on using this script, please see the Setting Up Outlook 365 section of the manual.

To enable debug logging, click this link. To disable debug logging, click this link. Debug logging is sent to Console and included in diagnostic reports.

To make the script check for new messages every 1 minute (the default), click this link. To make the script check for new messages every 15 seconds, click this link. To make the script check for new messages every 5 minutes, click this link.

Customizing the Script

If you need to customize the script, you will need to create your own application file rather than downloading the pre-made one. Open your edited script file Script Editor. From the File menu, choose Export…, set the File Format to Application, and make sure that Stay open after run handler is checked. Then click Save to create the Outlook - Filter Mailboxes.app application file.

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

Script

global gDebug, gSecondsBetweenChecks, gInboxSpamSieveName, gGoodCategoryName, gGoodFolderName, gMarkAsUnread
set _keys to {"OutlookScriptDebug", "OutlookFilterMailboxesSecondsBetweenChecks", "OutlookFilterMailboxesInboxSpamSieveName", "OutlookFilterMailboxesGoodCategoryName", "OutlookFilterMailboxesGoodFolderName", "OutlookMarkAsUnread"}
set _defaultValues to {false, 1 * 60, "InboxSpamSieve", "Good", "Inbox", false}
-- #17993: Cannot reproduce, but sometimes no error is reported instead of errAEEventNotPermitted, so try to prevent uninitalized values.
set {gDebug, gSecondsBetweenChecks, gInboxSpamSieveName, gGoodCategoryName, gGoodFolderName, gMarkAsUnread} to _defaultValues
set {gDebug, gSecondsBetweenChecks, gInboxSpamSieveName, gGoodCategoryName, gGoodFolderName, gMarkAsUnread} to my lookupDefaultsAndHandleError(_keys, _defaultValues)
my filterMailboxes()

on idle
    
-- This is executed periodically when the script is run as a stay-open application.
    
my filterMailboxes()
    
return gSecondsBetweenChecks
end idle

on reopen
    
-- This is executed when you click on the Dock icon after the script app is already running.
    
my filterMailboxes()
end reopen

on filterMailboxes()
    
if application "Microsoft Outlook" is not running then
        
my debugLog("Outlook is not running")
        
return
    
end if
    
try
        
set _mailboxes to my mailboxesToFilter()
        
repeat with _mailbox in _mailboxes
            
set _messages to my messagesToFilterFromMailbox(_mailbox)
            
if gDebug then
                
my debugLog((count of _messages) & " messages to filter in " & describeFolder(_mailbox))
            
end if
            
repeat with _message in _messages
                
set _score to scoreMessage(_message)
                
if _score ≥ 50 then
                    
my processSpamMessage(_message, _score)
                
else
                    
my processGoodMessage(_message, _score)
                
end if
            
end repeat
        
end repeat
    
on error _error number _errorNumber
        
my logToConsole("Error: " & _error)
        
if _errorNumber is -1743 then -- errAEEventNotPermitted
            
set _alertMessage to "You can give “Outlook Filter Mailboxes” access to control Outlook and SpamSieve from System Preferences > Security & Privacy > Privacy > Automation. (On macOS 13 Ventura this is in System Settings > Privacy & Security > Automation.) For more information, please see:

https://c-command.com/spamsieve/help/granting-automation-acc"
            
display alert _error message _alertMessage
        
end if
    
end try
end filterMailboxes

on mailboxesToFilter()
    
tell application "Microsoft Outlook"
        
set _result to every mail folder whose name starts with gInboxSpamSieveName
        
if _result is not {} then return _result
        
        
set _result to {}
        
set _accounts to imap accounts & pop accounts & exchange accounts -- Just "accounts" does not work.
        
repeat with _account in _accounts
            
copy _account's inbox to end of _result
        
end repeat
        
copy inbox to end of _result -- For Google accounts
    
end tell
    
return _result
end mailboxesToFilter

on messagesToFilterFromMailbox(_mailbox)
    
tell application "Microsoft Outlook"
        
if name of _mailbox starts with gInboxSpamSieveName then
            
with timeout of 2 * 60 seconds
                
return messages of _mailbox -- Faster than checking unread and category
            
end timeout
        
end if
        
set _messages to my unreadMessagesFromMailbox(_mailbox)
        
set _result to {}
        
repeat with _message in _messages
            
if my shouldFilterMessage(_message) then
                
copy _message to end of _result
            
end if
        
end repeat
        
return _result
    
end tell
end messagesToFilterFromMailbox

on unreadMessagesFromMailbox(_mailbox)
    
set _startDate to current date
    
tell application "Microsoft Outlook"
        
try
            
with timeout of 2 * 60 seconds
                
-- Using "whose" clause seems to make Outlook unresponsive.
                
set _allMessages to messages of _mailbox -- whose is read is false
            
end timeout
            
set _messages to {}
            
repeat with _message in _allMessages
                
if _message's is read is false then
                    
copy _message to end of _messages
                
end if
            
end repeat
        
on error _error number _errorNumber
            
my logToConsole("Outlook reported error “" & _error & "” (number " & _errorNumber & ") getting the messages from " & my describeFolder(_mailbox))
            
return {}
        
end try
    
end tell
    
set _endDate to current date
    
set _duration to _endDate - _startDate
    
set _statusMessage to "Outlook took " & _duration & " seconds to get " & (count of _messages) & " unread messages out of " & (count of _allMessages) & " total messages from " & my describeFolder(_mailbox)
    
if _duration > 3 then
        
my logToConsole(_statusMessage)
    
else
        
my debugLog(_statusMessage)
    
end if
    
return _messages
end unreadMessagesFromMailbox

on shouldFilterMessage(_message)
    
if gGoodCategoryName is "" then
        
return true
    
end if
    
return not my doesMessageHaveCategoryNamed(_message, gGoodCategoryName)
end shouldFilterMessage

on doesMessageHaveCategoryNamed(_message, _categoryName)
    
tell application "Microsoft Outlook"
        
set _categories to _message's category
        
repeat with _category in _categories
            
if _category's name is _categoryName then
                
return true
            
end if
        
end repeat
        
return false
    
end tell
end doesMessageHaveCategoryNamed

on scoreMessage(_message)
    
tell application "Microsoft Outlook"
        
set _source to _message's source
    
end tell
    
if _source is missing value then
        
my logToConsole("Outlook could not get the source of message: " & my subjectOfMessage(_message))
        
return 49
    
else
        
return my score(_source)
    
end if
end scoreMessage

on processSpamMessage(_message, _score)
    
my debugLogMessage("Predicted Spam (" & _score & ")", _message)
    
if my isSpamScoreUncertain(_score) then
        
my applyCategoryNamed(_message, "Uncertain Junk")
    
else
        
my applyCategoryNamed(_message, "Junk")
    
end if
    
my moveToSpamFolder(_message)
end processSpamMessage

on ensureUnread(_message, _beforeAfter)
    
if not gMarkAsUnread then return
    
tell application "Microsoft Outlook"
        
try
            
set _read to _message's is read
            
my debugLogMessage("Read status " & _beforeAfter & " moving: " & _read, _message)
            
if _read then
                
set _message's is read to false
                
set _read to _message's is read
                
my debugLogMessage("Read status after marking unread: " & _read, _message)
            
end if
        
on error _error
            
my debugLog("Error ensuring message is unread: " & _error)
        
end try
    
end tell
end ensureUnread

on moveToInbox(_message)
    
tell application "Microsoft Outlook"
        
try
            
set _inbox to inbox of _message's account
        
on error _errorMessage -- Will fail for new Google accounts that aren't scriptable.
            
my logToConsole("Error getting inbox of message so using generic inbox: " & _errorMessage)
            
try
                
my ensureUnread(_message, "before")
                
move _message to inbox
            
on error _errorMessage
                
my logToConsole("Error moving message to fallback generic inbox: " & _errorMessage)
            
end try
            
my ensureUnread(_message, "after")
            
return
        
end try
        
my debugLogMessage("Moving to inbox " & my describeFolder(_inbox), _message)
        
my ensureUnread(_message, "before")
        
move _message to _inbox
        
my ensureUnread(_message, "after")
    
end tell
end moveToInbox

on destinationFolderForMessage(_message)
    
tell application "Microsoft Outlook"
        
set _inboxFolderName to name of _message's folder
        
set AppleScript's text item delimiters to ""
        
set _start to (length of gInboxSpamSieveName) + 1
        
set _destinationName to (characters _start through -1 of _inboxFolderName) as Unicode text
        
set _account to _message's account
        
if _account is missing value then
            
return mail folder _destinationName
        
else
            
try
                
tell _account
                    
return mail folder _destinationName
                
end tell
            
on error -- If message is POP, the above won't search nested On My Computer folders, but this will.
                
return mail folder _destinationName
            
end try
        
end if
    
end tell
end destinationFolderForMessage

on moveToFolder(_message)
    
tell application "Microsoft Outlook"
        
try
            
set _folder to my destinationFolderForMessage(_message)
        
on error _errorMessage
            
my logToConsole("Error getting destination for message, so moving to inbox: " & _errorMessage)
            
my moveToInbox(_message)
            
return
        
end try
        
my debugLogMessage("Moving to folder " & my describeFolder(_folder), _message)
        
move _message to _folder
    
end tell
end moveToFolder

on moveToSpamFolder(_message)
    
tell application "Microsoft Outlook"
        
set _destFolder to my junkFolderForMessage(_message)
        
my debugLogMessage("Moving to junk mailbox " & my describeFolder(_destFolder), _message)
        
my ensureUnread(_message, "before")
        
move _message to _destFolder
        
try
            
if _message's folder is not _destFolder then
                
my debugLogMessage("Removing to junk mailbox " & my describeFolder(junk mail), _message)
                
move _message to junk mail
            
end if
        
end try
        
my ensureUnread(_message, "after")
    
end tell
end moveToSpamFolder

on junkFolderForMessage(_message)
    
tell application "Microsoft Outlook"
        
try
            
set _destFolder to junk mail of _message's account
            
if _destFolder is not missing value then return _destFolder
        
end try
        
try
            
set _destFolder to junk mail
            
if _destFolder is not missing value then return _destFolder
        
end try
        
try
            
set _destFolder to folder "Junk E-mail" of _message's account
            
if _destFolder is not missing value then return _destFolder
        
end try
        
try
            
set _destFolder to folder "Junk" of _message's account
            
if _destFolder is not missing value then return _destFolder
        
end try
        
display dialog "Could not find the “Junk E-mail” folder"
        
return junk mail
    
end tell
end junkFolderForMessage

on processGoodMessage(_message, _score)
    
my debugLogMessage("Predicted Good (" & _score & ")", _message)
    
tell application "Microsoft Outlook"
        
set _folderName to name of _message's folder
        
if _folderName is gInboxSpamSieveName then
            
my moveToInbox(_message)
        
else if _folderName starts with gInboxSpamSieveName then
            
my moveToFolder(_message)
        
else
            
if gGoodCategoryName is not "" then
                
my applyCategoryNamed(_message, gGoodCategoryName)
            
end if
            
if gGoodFolderName is not "" then
                
my moveToFolderNamed(_message, gGoodFolderName)
            
end if
        
end if
    
end tell
end processGoodMessage

on isSpamScoreUncertain(_score)
    
tell application "SpamSieve"
        
set _keys to {"Border", "OutlookUncertainJunk"}
        
set _defaults to {75, true}
        
try
            
set {gUncertainThreshold, gUncertainJunk} to lookup keys _keys default values _defaults
        
on error
            
set {gUncertainThreshold, gUncertainJunk} to _defaults
        
end try
    
end tell
    
return _score < gUncertainThreshold and gUncertainJunk
end isSpamScoreUncertain

on subjectOfMessage(_message)
    
tell application "Microsoft Outlook"
        
try
            
return (_message's subject) as Unicode text
        
on error
            
return "<Error getting subject of message id " & _message's id & ">"
        
end try
    
end tell
end subjectOfMessage

on moveToFolderNamed(_message, _folderName)
    
tell application "Microsoft Outlook"
        
try
            
move _message to folder _folderName of _message's account
        
on error _error
            
-- Folder probably doesn't exist. Not sure how to create it.
            
my logToConsole("Error moving to " & _folderName & ": " & _error)
        
end try
    
end tell
end moveToFolderNamed

-- Categories

on categoryForName(_categoryName)
    
tell application "Microsoft Outlook"
        
try
            
-- "exists category _categoryName" sometimes lies
            
return category _categoryName
        
on error
            
try
                
-- getting by name doesn't always work
                
repeat with _category in categories
                    
if _category's name is _categoryName then return _category
                
end repeat
            
end try
            
set _category to make new category with properties {name:_categoryName}
            
if _categoryName is gGoodCategoryName then
                
set _category's color to {0, 0, 0}
            
end if
        
end try
        
return category _categoryName
    
end tell
end categoryForName

on applyCategoryNamed(_message, _categoryName)
    
tell application "Microsoft Outlook"
        
set _categoryToApply to my categoryForName(_categoryName)
        
set _categories to _message's category
        
repeat with _category in _categories
            
if _category's id is equal to _categoryToApply's id then return
        
end repeat
        
set category of _message to {_categoryToApply} & category of _message
    
end tell
end applyCategoryNamed

-- Logging

on debugLogMessage(_string, _message)
    
if not gDebug then return
    
tell application "Microsoft Outlook"
        
try
            
set _location to my describeFolder(_message's folder)
        
on error
            
set _location to "<error getting message's mailbox>"
        
end try
        
set _subject to my subjectOfMessage(_message)
    
end tell
    
my debugLog(_string & ": [" & _location & "] " & _subject)
end debugLogMessage


on describeFolder(_folder)
    
tell application "Microsoft Outlook"
        
set _container to _folder's container -- For some reason, this doesn't work inside the "try"
        
try
            
set _containerName to my describeFolder(_container)
        
on error
            
try
                
return name of _folder's account
            
on error
                
return "[Unknown/OnMyComputer/Google]"
            
end try
        
end try
        
set _folderName to _folder's name
        
return _containerName & "/" & _folderName
    
end tell
end describeFolder

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

on logToConsole(_message)
    
set _prefix to "SpamSieve [Outlook Filter Mailboxes] MJTLog"
    
set _logMessage to _prefix & " " & _message
    
try
        
do shell script "/usr/bin/logger -s " & _logMessage's quoted form
    
on error _error number _errorNumber
        
set _alertMessage to "An error occurred while logging the message:" & return & return & _message & return & return & "to Console. Error " & _errorNumber & ": " & return & return & _error & return & return & "This error message will dismiss itself after 10 seconds so that your filtering is not interrupted."
        
display alert _prefix message _alertMessage giving up after 10
    
end try
end logToConsole

on lookupDefaultsAndHandleError(_keys, _defaultValues)
    
try
        
return my lookupDefaults(_keys, _defaultValues)
    
on error _error number _errorNumber
        
if _errorNumber is -1743 then -- errAEEventNotPermitted
            
my logToConsole("Error looking up defaults: " & _error)
            
set _alertMessage to "You can give “Outlook Filter Mailboxes” access to control Outlook and SpamSieve from System Preferences > Security & Privacy > Privacy > Automation. (On macOS 13 Ventura this is in System Settings > Privacy & Security > Automation.) For more information, please see:

https://c-command.com/spamsieve/help/granting-automation-acc"
            
display alert _error message _alertMessage
        
end if
        
error _error number _errorNumber
    
end try
end lookupDefaultsAndHandleError

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
            
try
                
return lookup keys _keys default values _defaultValues
            
on error _errorMessage
                
my logToConsole("Error getting fallback defaults: " & _errorMessage)
                
return _defaultValues
            
end try
        
end try
    
end tell
end lookupDefaults

on score(_source)
    
my debugLog("Begin scoring message of length " & (count of _source))
    
tell application "SpamSieve"
        
set _result to score message _source
    
end tell
    
my debugLog("End scoring message of length " & (count of _source))
    
return _result
end score