Skip to content

Commit 6faaf1f

Browse files
authored
Merge pull request #5 from chsturm/v1.3.1
V1.3.1
2 parents 3ba2325 + c0c12ef commit 6faaf1f

5 files changed

Lines changed: 83 additions & 70 deletions

File tree

INSTALL

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ This spamfilter lets you easily define keyword-based filter rules for each of yo
66
INSTALLATION
77
There are two invokation modes for the spamfilter script that impose different installation tasks. The first mode relies on Mail.app's rule infrastructure to automate handling of new incoming messages dedicated to default inboxes:
88

9-
1. Download spamfilter.zipfrom Releases
9+
1. Download spamfilter.zip from Releases
1010
2. Extract zip archive, open Terminal and change working directory to spamfilter directory via 'cd path/to/spamfilter_dir'
1111
3. Run 'sh install.sh'
1212
4. Open Mail.app's preferences pane and go to "Rules"

LICENSE

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
spamfilter for Apple Mail.app
2-
Copyright (c) 2022 Christian Sturm
2+
Copyright (c) 2023 Christian Sturm
33

44
This program is free software: you can redistribute it and/or modify
55
it under the terms of the GNU General Public License as published by

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ Spam messages are marked as Junk and moved to the trash folder.
44

55
## Installation
66
There are two invokation modes for the spamfilter script that impose different installation tasks. The first mode relies on Mail.app's rule infrastructure to automate handling of new incoming messages dedicated to default inboxes:
7-
1. Download `spamfilter.zip`from [Releases](https://github.com/chsturm/spamfilter/releases)
7+
1. Download `spamfilter.zip` from [Releases](https://github.com/chsturm/spamfilter/releases)
88
2. Extract zip archive, open Terminal and change working directory to spamfilter directory via `cd path/to/spamfilter`
99
3. Run `sh install.sh`
1010
4. Open Mail.app's preferences pane and go to "Rules"

com.github.chsturm.spamfilter.plist

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
</array>
1212

1313
<key>RunAtLoad</key>
14-
<true/>
14+
<false/>
1515
<key>StartInterval</key>
1616
<integer>LAUNCH_INTERVAL</integer>
1717
</dict>

spamfilter.applescript

Lines changed: 79 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/*
22
spamfilter for Apple Mail.app
3-
Copyright (c) 2022 Christian Sturm
3+
Copyright (c) 2023 Christian Sturm
44

55
This program is free software: you can redistribute it and/or modify
66
it 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
2626
const shouldLogActivity = false // true: log details about message tests to file
2727
const mutexLifetime = 600 // duration in seconds after which a mutex lock will be reset
2828

29+
2930
const mail = Application.currentApplication().name == "Mail"
3031
? Application.currentApplication() : Application("Mail")
32+
mail.includeStandardAdditions = true
3133
if (!mail.running()) {
3234
delay(10)
3335
if (!mail.running()) throw "Mail.app not running"
3436
}
35-
mail.includeStandardAdditions = true
37+
3638

3739
ObjC.import('Foundation')
3840
//ObjC.import('stdlib')
3941
ObjC.import('stdio')
4042
ObjC.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 = ['.7z', '.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 = ['.7z', '.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> */
89117
function 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 */
121150
function 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
*/
199229
SpamFilterHandler.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 */
229260
SpamFilterHandler.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
*/
265295
SpamFilterHandler.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

Comments
 (0)