Outlook - Filter Mailboxes

Summary: Periodically filter new messages in the inbox with SpamSieve.
Requires: SpamSieve, Outlook 2016
Install Location: /Applications
Last Modified: 2017-10-09

Description

This script application is to help SpamSieve to automatically filter new messages in Outlook 2016, 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 2016 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 5 minutes (the default), click this link. To make the script check for new messages every 1 minute, 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, gExtraInboxName
set _keys to {"OutlookScriptDebug", "OutlookFilterMailboxesSecondsBetweenChecks", "OutlookFilterMailboxesInboxSpamSieveName", "OutlookFilterMailboxesGoodCategoryName", "OutlookFilterMailboxesGoodFolderName", "OutlookFilterMailboxesExtraInboxName"}
set _defaultValues to {false, 5 * 60, "InboxSpamSieve", "Good", "", "Inbox"}
set {gDebug, gSecondsBetweenChecks, gInboxSpamSieveName, gGoodCategoryName, gGoodFolderName, gExtraInboxName} to my lookupDefaults(_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 mailboxNamesToFilter()
    
return {"Inbox", "INBOX", "Indbakke", "innboks", "Posteingang", "Boite de reception", "Inkorg", "Postvak IN", gExtraInboxName}
end mailboxNamesToFilter

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
        
my logToConsole("Error: " & _error)
    
end try
end filterMailboxes

on mailboxesToFilter()
    
tell application "Microsoft Outlook"
        
set _result to every mail folder whose name is gInboxSpamSieveName
        
if _result is not {} then return _result
    
end tell
    
    
set _result to {}
    
set _names to my mailboxNamesToFilter()
    
repeat with _name in _names
        
tell application "Microsoft Outlook"
            
considering case
                
try
                    
set _matches to (every mail folder whose name is _name)
                    
set _result to _result & _matches
                
on error -- _matches may not be defined
                    
my logToConsole("Working around Outlook error getting folders named: " & _name)
                    
set _folders to every mail folder
                    
repeat with _folder in _folders
                        
if name of _folder is _name then
                            
copy _folder to end of _result
                        
end if
                    
end repeat
                
end try
            
end considering
        
end tell
    
end repeat
    
return _result
end mailboxesToFilter

on messagesToFilterFromMailbox(_mailbox)
    
tell application "Microsoft Outlook"
        
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
        
tell application "SpamSieve"
            
return score message _source
        
end tell
    
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 moveToInbox(_message)
    
tell application "Microsoft Outlook"
        
set _inbox to inbox of _message's account
        
my debugLogMessage("Moving to inbox " & my describeFolder(_inbox), _message)
        
move _message to _inbox
    
end tell
end moveToInbox

on moveToSpamFolder(_message)
    
tell application "Microsoft Outlook"
        
set _destFolder to my junkFolderForMessage(_message)
        
my debugLogMessage("Moving to junk mailbox " & my describeFolder(_destFolder), _message)
        
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
    
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"
        
if name of _message's folder is gInboxSpamSieveName then
            
my moveToInbox(_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 "On My Computer"
            
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]"
    
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 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