11/*
22spamfilter for Apple Mail.app
3- Copyright (c) 2022 Christian Sturm
3+ Copyright (c) 2023 Christian Sturm
44
55This program is free software: you can redistribute it and / or modify
66it under the terms of the GNU General Public License as published by
@@ -26,70 +26,99 @@ var shouldAlertMatchDetails = false // true: alert rule item if a rule match is
2626const shouldLogActivity = false // true : log details about message tests to file
2727const mutexLifetime = 600 // duration in seconds after which a mutex lock will be reset
2828
29+
2930const mail = Application .currentApplication().name == " Mail"
3031 ? Application .currentApplication() : Application (" Mail" )
32+ mail.includeStandardAdditions = true
3133if (!mail.running ()) {
3234 delay (10 )
3335 if (!mail.running ()) throw " Mail.app not running"
3436}
35- mail.includeStandardAdditions = true
37+
3638
3739ObjC.import('Foundation')
3840// ObjC.import('stdlib')
3941ObjC.import('stdio')
4042ObjC.import('unistd')
4143
44+ var rulesHandler = new RulesHandler()
4245
43- /** load blacklist rules */
44- const path = mail.pathTo(" library folder" , {from : " user domain" , folderCreation : false }).toString() + " /Application Scripts/com.apple.mail/spamfilter-rules.json"
45- const rulesList = loadRules(path)
4646
47- function loadRules (path) {
48- try {
49- var fm = $.NSFileManager.defaultManager
50- if (!fm.fileExistsAtPath(path)) {
51- mail.displayDialog(" No rules file found!" , {withIcon : " caution" , givingUpAfter : 10 })
52- }
53- var contents = fm.contentsAtPath(path) // NSData
54- contents = $.NSString.alloc.initWithDataEncoding(contents , $.NSUTF8StringEncoding);
55- var configJsonStr = ObjC.unwrap(contents )
47+ /** These chars are usually not used within normal text ,
48+ but to prevent word - based blacklisting in spam.
49+ e.g. zero- width spaces like byte order mark
50+ */
51+ const cheatChars = ['\uFEFF','\u200B', '\u200C', '\u2060']
52+
53+ /** uncommon file extensions */
54+ const fileExtensions = ['.7 z', '.exe', '.jpg.zip']
55+
56+ /** uncommon charsets (in lowercase) */
57+ const charsetBlacklist = ['windows-1251 '/* cyrillic*/ , 'gb2312'/* chinese*/ , 'gb18030'/* chinese*/ ]
58+
59+
60+ /** Construct blacklist rules handler */
61+ function RulesHandler(path = null ) {
62+ this.rulesList = null
63+ if (path)
64+ this.path = path
65+ else {
66+ this.path = mail.pathTo(" library folder" , {from : " user domain" , folderCreation : false }).toString() + " /Application Scripts/com.apple.mail/spamfilter-rules.json"
67+ }
68+ }
69+
70+ /** Load json object from rules file */
71+ RulesHandler.prototype.loadConfigFromFile = function() {
72+ var config = null ,
73+ fm = $.NSFileManager.defaultManager
74+ if (!fm.fileExistsAtPath(this.path)) {
75+ mail.displayDialog(" No rules file found!" , {withIcon : " caution" , givingUpAfter : 10 })
76+ return config
77+ }
78+ var contents = fm.contentsAtPath(this.path) // NSData
79+ contents = $.NSString.alloc.initWithDataEncoding(contents , $.NSUTF8StringEncoding);
80+ var configJsonStr = ObjC.unwrap(contents )
5681
57- if (configJsonStr != "" )
58- var config = JSON.parse(configJsonStr)
59- else
60- console.log (" No rules found!" )
82+ if (configJsonStr != "" )
83+ config = JSON.parse(configJsonStr)
84+ else
85+ console.log (" No rules in file!" )
86+ return config
87+ }
88+
89+ /** Setup rules and configuration from json object */
90+ RulesHandler.prototype.loadRulesList = function() {
91+ try {
92+ var config = this.loadConfigFromFile()
6193 } catch (e) {
6294 console.log (e.name + ': '+ e.message)
6395 if (e instanceof SyntaxError && !config)
6496 mail.displayDialog(" JSON syntax error in rules file on line " + e.lineNumber + " : "
6597 + e.message)
6698 }
6799
68- if (!config || !config.rulesList) return []
100+ if (!config || !config.rulesList) return false
101+
69102 if (config.shouldAlertMatchDetails === true || config.shouldAlertMatchDetails !== " false" )
70103 shouldAlertMatchDetails = config.shouldAlertMatchDetails
71- return config.rulesList
104+ this.rulesList = config.rulesList
105+ return true
72106}
73107
74-
75- /** These chars are usually not used within normal text ,
76- but to prevent word - based blacklisting in spam.
77- e.g. zero- width spaces like byte order mark
78- */
79- const cheatChars = ['\uFEFF','\u200B', '\u200C', '\u2060']
80-
81- /** uncommon file extensions */
82- const fileExtensions = ['.7 z', '.exe', '.jpg.zip']
83-
84- /** uncommon charsets (in lowercase) */
85- const charsetBlacklist = ['windows-1251 '/* cyrillic*/ , 'gb2312'/* chinese*/ , 'gb18030'/* chinese*/ ]
108+ /** Get rules for given email address resp. account */
109+ RulesHandler.prototype.getRulesForAddress = function(address) {
110+ return this.rulesList.find(function(rule) {
111+ return address === rule.email
112+ })
113+ }
86114
87115
88116/** handler called by terminal via osascript - l JavaScript < path> */
89117function run () {
90118 mail.downloadHtmlAttachments = false
91119 const accountList = mail.accounts()
92120 var shouldDisplayNotification = false
121+ if (!rulesHandler.loadRulesList()) return
93122
94123 accountList.forEach(function(account){
95124 if (account.enabled() === false ) return
@@ -120,6 +149,7 @@ function run () {
120149/** handler called by Apple Mail when applying rules on messages */
121150function performMailActionWithMessages (messages, manualProperties) {
122151 mail.downloadHtmlAttachments = false
152+ if (!rulesHandler.loadRulesList()) return
123153
124154 // skip remaining messages if identical to first one due to bug in Mail.app
125155 // wrap Mail JXA API
@@ -197,7 +227,7 @@ function SpamFilterHandler () {
197227 Defines mailbox, rules and account properties for subsequent filtering
198228*/
199229SpamFilterHandler.prototype.filterMessageList = function(messageList) {
200- if (!Array.isArray(rulesList)) {
230+ if (!Array.isArray(rulesHandler. rulesList)) {
201231 mail.displayDialog(" No rules list found in json file" )
202232 return
203233 }
@@ -226,6 +256,7 @@ SpamFilterHandler.prototype.filterMessageList = function(messageList) {
226256 }
227257}
228258
259+ /** Get account rules from general rules list */
229260SpamFilterHandler.prototype.loadAccountRules = function() {
230261 if (!this.account) {
231262 ActivityLog.log (" loadAccountRules() failed: this.account not defined" );
@@ -234,11 +265,10 @@ SpamFilterHandler.prototype.loadAccountRules = function() {
234265 const accountAddressList = this.account.emailAddressList
235266
236267 // search account specific rules object
237- const accountRules = rulesList.find(function(rule) {
238- return accountAddressList.some (function(address){
239- return address === rule.email
240- })
241- })
268+ var accountRules = null
269+ for (let address of accountAddressList) {
270+ if (accountRules = rulesHandler.getRulesForAddress(address)) break
271+ }
242272 if (!accountRules) return false
243273 this.accountRules = accountRules
244274
@@ -264,36 +294,19 @@ SpamFilterHandler.prototype.loadAccountRules = function() {
264294*/
265295SpamFilterHandler.prototype.getRuleAndAccountFromMailbox = function(mailbox) {
266296 this.account = mailbox.account
267- const boxName = mailbox.name /* ,
268- accountAddressList = this.account.emailAddressList
269-
270- // search account specific rules object
271- const accountRules = rulesList.find(function(rule) {
272- return accountAddressList.some (function(address){
273- return address === rule.email
274- })
275- })
276- if (!accountRules) return null */
297+ const boxName = mailbox.name
298+
277299 if (!this.loadAccountRules()) return null ;
278300
279301 // choose either the default rule for INBOX or one for cutom mailboxes
280302 let rule = null
281- /* if (boxName.includes('INBOX')) {
282- rule = {email : this.accountRules.email,
283- fromWhitelist : this.accountRules.fromWhitelist,
284- senderBlacklist : this.accountRules.senderBlacklist,
285- subjectBlacklist : this.accountRules.subjectBlacklist,
286- contentBlacklist : this.accountRules.contentBlacklist
287- }
288- } else */ if (Array.isArray(this.accountRules.mailboxList)
303+ if (Array.isArray(this.accountRules.mailboxList)
289304 && this.accountRules.mailboxList.length > 0 ) {
290305 rule = this.accountRules.mailboxList.find(function(rule){
291306 return boxName === rule.name
292307 })
293308 if (rule) rule.email = this.accountRules.email
294309 }
295-
296- // this.accountRules = accountRules
297310 return rule
298311}
299312
@@ -436,7 +449,7 @@ SpamFilterHandler.prototype.filterAccountMailboxes = function() {
436449
437450
438451/** log all message tests in separate file for debugging if shouldLogActivity == true */
439- const ActivityLog = (function () {
452+ const ActivityLog = (function() {
440453 if (!shouldLogActivity) {
441454 // return dummy methods if logging switched off
442455 const dummyFnc = function(){}
@@ -469,7 +482,7 @@ const ActivityLog = (function () {
469482 }
470483
471484 /** general log function appending entry as a line to file */
472- var log = function (str) {
485+ var log = function(str) {
473486 try {
474487 fh.seekToEndOfFile
475488 fh.writeData(ObjC.wrap(str + " \n " ).dataUsingEncoding($.NSUTF8StringEncoding))
@@ -482,14 +495,14 @@ const ActivityLog = (function () {
482495 }
483496
484497 /** log given message along with run type of test */
485- var logMessage = function (msg, runType) {
498+ var logMessage = function(msg, runType) {
486499 log (runType + " ,ts." + Date .now() + " : " + msg.getField('dateReceived')
487500 + " ,id." + msg.id + " ,box." + msg.mailbox.name + " ," +
488501 msg.getField('sender') + " , " + msg.getField('subject'))
489502 }
490503
491504 /** close file before quit */
492- var finish = function () {
505+ var finish = function() {
493506 try {
494507 fh.closeFile
495508 } catch (e) {
@@ -504,7 +517,7 @@ const ActivityLog = (function () {
504517})()
505518
506519/** manages mutex locks accessible to different spamfilter instances (osascript processes) */
507- const RunCoordinator = (function () {
520+ const RunCoordinator = (function() {
508521 const dir = mail.pathTo(" library folder" , {from : " user domain" , folderCreation : false }
509522 ).toString() + " /Application Scripts/com.apple.mail/"
510523 var path = '', mutex = null , gotLock = null
@@ -515,7 +528,7 @@ const RunCoordinator = (function () {
515528 }
516529
517530 /** try to get lock for specified resource id and return result */
518- RunCoordinator.prototype.tryLock = function () {
531+ RunCoordinator.prototype.tryLock = function() {
519532 mutex = $.NSDistributedLock.lockWithPath(path)
520533
521534 // force unlock if older than mutexLifetime (600 ) sec as normal unlocking seemed to fail
@@ -536,12 +549,12 @@ const RunCoordinator = (function () {
536549 }
537550
538551 /** returns true if got lock else false ; null if tryLock() not yet called */
539- RunCoordinator.prototype.gotLock = function () {
552+ RunCoordinator.prototype.gotLock = function() {
540553 return gotLock
541554 }
542555
543556 /** unlock existing mutex */
544- RunCoordinator.prototype.unlock = function () {
557+ RunCoordinator.prototype.unlock = function() {
545558 if (mutex) mutex.unlock
546559 }
547560
0 commit comments