25 |
26 |
27 |
--------------------------------------------------------------------------------
/test/mail-examples/spam-status-3.eml:
--------------------------------------------------------------------------------
1 | To: receiver@spamscores
2 | From: sender@spamscores
3 | Subject: [X-Spam-Status] Spam Scores test mail (spam-status-3.eml)
4 | X-Spam-Checker-Version: SpamAssassin 3.4.0 (2014-02-07) on srv01.spamscores
5 | X-Spam-Level:
6 | X-Spam-Status: No, score=-42.42 required=3.0 tests=R_DKIM_ALLOW,R_MIXED_CHARSET,
7 | FREEMAIL_FROM,DKIM_TRACE,MX_GOOD,BAYES_HAM
8 | autolearn=ham autolearn_force=no version=3.4.0
9 | Message-ID: spam-status-3
10 |
11 | This is a message to test the Spam Scores Thunderbird add-on.
12 |
13 | --
14 | To: receiver@spamscores
15 | From: sender@spamscores
16 | Subject: [X-Spam-Status] Spam Scores test mail (spam-status-3.eml)
17 | X-Spam-Checker-Version: SpamAssassin 3.4.0 (2014-02-07) on srv01.spamscores
18 | X-Spam-Level:
19 | X-Spam-Status: No, score=-42.42 required=3.0 tests=R_DKIM_ALLOW,R_MIXED_CHARSET,
20 | FREEMAIL_FROM,DKIM_TRACE,MX_GOOD,BAYES_HAM
21 | autolearn=ham autolearn_force=no version=3.4.0
22 | Message-ID: spam-status-3
--------------------------------------------------------------------------------
/src/typedef/globals.js:
--------------------------------------------------------------------------------
1 | // FIREFOX GLOBALS
2 |
3 | // https://firefox-source-docs.mozilla.org/toolkit/components/extensions/webextensions/basics.html#globals-available-in-the-api-scripts-global
4 |
5 | /**
6 | * Services namespace
7 | * Based on Mozilla xpCOM
8 | * https://developer.mozilla.org/en-US/docs/Mozilla/Firefox/Releases/57#changes_for_add-on_and_mozilla_developers
9 | * @namespace
10 | * @property {object} xulStore
11 | * @property {object} obs https://web.archive.org/web/20210603143450/https://developer.mozilla.org/en-US/docs/Mozilla/Tech/XPCOM/Reference/Interface/nsIObserverService
12 | * @property {object} scriptloader
13 | */
14 | const Services = {}
15 |
16 | /**
17 | * AppConstants
18 | * @namespace
19 | * @property {number} MOZ_APP_VERSION Thunderbird Version
20 | */
21 | const AppConstants = {}
22 |
23 | /**
24 | * WebExtensionPolicy
25 | * @namespace
26 | */
27 | const WebExtensionPolicy = {
28 | /**
29 | *
30 | * @param {*} id
31 | * @returns {extension}
32 | */
33 | getByID: id => {}
34 | }
35 |
--------------------------------------------------------------------------------
/test/mail-examples/spam-status-2.eml:
--------------------------------------------------------------------------------
1 | To: receiver@spamscores
2 | From: sender@spamscores
3 | Subject: [X-Spam-Status] Spam Scores test mail (spam-status-2.eml)
4 | X-Spam-Flag: NO
5 | X-Spam-Score: -42.42
6 | X-Spam-Level:
7 | X-Spam-Status: No, score=-42.42 tagged_above=-9999 required=3
8 | tests=[R_DKIM_ALLOW=-0.2, R_MIXED_CHARSET=0.18,
9 | FREEMAIL_FROM=0.00, DKIM_TRACE=-0.00, MX_GOOD=-0.01,
10 | BAYES_HAM=-5.5] autolearn=ham autolearn_force=no
11 | Message-ID: spam-status-2
12 | X-Spam-Flag: NO
13 |
14 | This is a message to test the Spam Scores Thunderbird add-on.
15 |
16 | --
17 | To: receiver@spamscores
18 | From: sender@spamscores
19 | Subject: [X-Spam-Status] Spam Scores test mail (spam-status-2.eml)
20 | X-Spam-Flag: NO
21 | X-Spam-Score: -42.42
22 | X-Spam-Level:
23 | X-Spam-Status: No, score=-42.42 tagged_above=-9999 required=3
24 | tests=[R_DKIM_ALLOW=-0.2, R_MIXED_CHARSET=0.18,
25 | FREEMAIL_FROM=0.00, DKIM_TRACE=-0.00, MX_GOOD=-0.01,
26 | BAYES_HAM=-5.5] autolearn=ham autolearn_force=no
27 | Message-ID: spam-status-2
28 | X-Spam-Flag: NO
--------------------------------------------------------------------------------
/test/mail-examples/spam-status-7.eml:
--------------------------------------------------------------------------------
1 | To: receiver@spamscores
2 | From: sender@spamscores
3 | Subject: [X-Spam-Status] Spam Scores test mail (spam-status-7.eml)
4 | X-Spam-Report: -42.42 hits, 9 required;
5 | * -0.2 -- DKIM verification succeed
6 | * 0.18 -- Mixed characters in a message
7 | * 0.00 -- From is a Freemail address
8 | [gmx.de]
9 | * -0.00 -- DKIM checks completed
10 | * -0.01 -- Domain has working MX
11 | * -5.5 -- Message probably ham
12 | Message-ID: spam-status-7
13 |
14 | This is a message to test the Spam Scores Thunderbird add-on.
15 |
16 | --
17 | To: receiver@spamscores
18 | From: sender@spamscores
19 | Subject: [X-Spam-Status] Spam Scores test mail (spam-status-7.eml)
20 | X-Spam-Report: -42.42 hits, 9 required;
21 | * -0.2 -- DKIM verification succeed
22 | * 0.18 -- Mixed characters in a message
23 | * 0.00 -- From is a Freemail address
24 | [gmx.de]
25 | * -0.00 -- DKIM checks completed
26 | * -0.01 -- Domain has working MX
27 | * -5.5 -- Message probably ham
28 | Message-ID: spam-status-7
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a report to help improve the project
4 | title: ''
5 | labels: bug
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Describe the bug**
11 | A clear and concise description of what the bug is.
12 |
13 | **Reproduction steps**
14 | Steps to reproduce the behavior.
15 |
16 | **Expected behavior**
17 | A clear and concise description of what you expected to happen.
18 |
19 | **Screenshots**
20 | If applicable, add screenshots to help explain your problem.
21 |
22 | **Thunderbird (please complete the following information):**
23 | - OS: [e.g. Windows 10]
24 | - Version: [e.g. 78.7.0]
25 |
26 | **Spam Score add-on (please complete the following information):**
27 | - Version: [e.g. 1.1.5] - visible under Extras -> Add-ons -> Spam Scores
28 |
29 | **Additional context**
30 | Add any other context about the problem here. If, for example, no spam score is visible in the list view column, please provide an example of a header of any of these mails (blank out private information like IP or mail addresses).
31 |
--------------------------------------------------------------------------------
/test/mail-examples/spam-status-1.eml:
--------------------------------------------------------------------------------
1 | To: receiver@spamscores
2 | From: sender@spamscores
3 | Subject: [X-Spam-Status] Spam Scores test mail (spam-status-1.eml)
4 | X-Spam-Status: No, score=-42.42
5 | X-Spam-Score: -42.42
6 | X-Spam-Bar: +
7 | X-Spam-Report: Action: no action
8 | Symbol: R_DKIM_ALLOW(-0.2)
9 | Symbol: R_MIXED_CHARSET(0.18)
10 | Symbol: FREEMAIL_FROM(0.00)
11 | Symbol: DKIM_TRACE(-0.00)
12 | Symbol: MX_GOOD(-0.01)
13 | Symbol: BAYES_HAM(-5.5)
14 | Message-ID: spam-status-1
15 | X-Spam-Flag: NO
16 |
17 | This is a message to test the Spam Scores Thunderbird add-on.
18 |
19 | --
20 | To: receiver@spamscores
21 | From: sender@spamscores
22 | Subject: [X-Spam-Status] Spam Scores test mail (spam-status-1.eml)
23 | X-Spam-Status: No, score=-42.42
24 | X-Spam-Score: -42.42
25 | X-Spam-Bar: +
26 | X-Spam-Report: Action: no action
27 | Symbol: R_DKIM_ALLOW(-0.2)
28 | Symbol: R_MIXED_CHARSET(0.18)
29 | Symbol: FREEMAIL_FROM(0.00)
30 | Symbol: DKIM_TRACE(-0.00)
31 | Symbol: MX_GOOD(-0.01)
32 | Symbol: BAYES_HAM(-5.5)
33 | Message-ID: spam-status-1
34 | X-Spam-Flag: NO
--------------------------------------------------------------------------------
/test/mail-examples/mailscanner-spamscore-1.eml:
--------------------------------------------------------------------------------
1 | To: receiver@spamscores
2 | From: sender@spamscores
3 | Subject: [X-MyCompany-MailScanner-SpamScore] Spam Scores test mail (mailscanner-spamscore-1.eml)
4 | X-MyCompany-MailScanner-From: receiver@spamscores
5 | X-MyCompany-MailScanner-SpamScore: ssssssss
6 | X-MyCompany-MailScanner-SpamCheck: spam, SpamAssassin (not cached, score=-42.42,
7 | required 3, R_DKIM_ALLOW -0.2, R_MIXED_CHARSET 0.18,
8 | FREEMAIL_FROM 0.00, DKIM_TRACE -0.00, MX_GOOD -0.01,
9 | BAYES_HAM -5.5)
10 | Message-ID: mailscanner-spamscore-1
11 |
12 | This is a message to test the Spam Scores Thunderbird add-on.
13 |
14 | --
15 | To: receiver@spamscores
16 | From: sender@spamscores
17 | Subject: [X-MyCompany-MailScanner-SpamScore] Spam Scores test mail (mailscanner-spamscore-1.eml)
18 | X-MyCompany-MailScanner-From: receiver@spamscores
19 | X-MyCompany-MailScanner-SpamScore: ssssssss
20 | X-MyCompany-MailScanner-SpamCheck: spam, SpamAssassin (not cached, score=-42.42,
21 | required 3, R_DKIM_ALLOW -0.2, R_MIXED_CHARSET 0.18,
22 | FREEMAIL_FROM 0.00, DKIM_TRACE -0.00, MX_GOOD -0.01,
23 | BAYES_HAM -5.5)
24 | Message-ID: mailscanner-spamscore-1
--------------------------------------------------------------------------------
/test/mail-examples/spam-status-4.eml:
--------------------------------------------------------------------------------
1 | To: receiver@spamscores
2 | From: sender@spamscores
3 | Subject: [X-Spam-Status] Spam Scores test mail (spam-status-4.eml)
4 | X-Spam-Checker-Version: SpamAssassin 3.4.2 (2018-09-13) on srv01.spamscores
5 | X-Spam-Level:
6 | X-Spam-Status: No/-42.42/5.0
7 | X-Spam-Report: Action: no action
8 | Symbol: R_DKIM_ALLOW(-0.2)
9 | Symbol: R_MIXED_CHARSET(0.18)
10 | Symbol: FREEMAIL_FROM(0.00)
11 | Symbol: DKIM_TRACE(-0.00)
12 | Symbol: MX_GOOD(-0.01)
13 | Symbol: BAYES_HAM(-5.5)
14 | Message-ID: spam-status-4
15 | X-Spam-Flag: NO
16 |
17 | This is a message to test the Spam Scores Thunderbird add-on.
18 |
19 | --
20 | To: receiver@spamscores
21 | From: sender@spamscores
22 | Subject: [X-Spam-Status] Spam Scores test mail (spam-status-4.eml)
23 | X-Spam-Checker-Version: SpamAssassin 3.4.2 (2018-09-13) on srv01.spamscores
24 | X-Spam-Level:
25 | X-Spam-Status: No/-42.42/5.0
26 | X-Spam-Report: Action: no action
27 | Symbol: R_DKIM_ALLOW(-0.2)
28 | Symbol: R_MIXED_CHARSET(0.18)
29 | Symbol: FREEMAIL_FROM(0.00)
30 | Symbol: DKIM_TRACE(-0.00)
31 | Symbol: MX_GOOD(-0.01)
32 | Symbol: BAYES_HAM(-5.5)
33 | Message-ID: spam-status-4
34 | X-Spam-Flag: NO
--------------------------------------------------------------------------------
/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "manifest_version": 2,
3 | "name": "__MSG_extensionName__",
4 | "description": "__MSG_extensionDescription__",
5 | "version": "1.6.0",
6 | "author": "Christian Zaenker",
7 | "default_locale": "en",
8 | "applications": {
9 | "gecko": {
10 | "id": "spamscores@czaenker",
11 | "strict_min_version": "115.0",
12 | "strict_max_version": "144.*"
13 | }
14 | },
15 | "icons": {
16 | "64": "images/icon.png",
17 | "32": "images/icon-32px.png",
18 | "16": "images/icon-16px.png"
19 | },
20 | "background": {
21 | "page": "src/background/background.html"
22 | },
23 | "permissions": ["messagesRead", "accountsRead", "storage"],
24 | "experiment_apis": {
25 | "SpamScores": {
26 | "schema": "src/experiments/schema.json",
27 | "parent": {
28 | "scopes": ["addon_parent"],
29 | "paths": [["SpamScores"]],
30 | "script": "src/experiments/experiments.js",
31 | "events": ["startup"]
32 | }
33 | }
34 | },
35 | "message_display_action": {
36 | "default_popup": "src/browser/popup/popup.html",
37 | "default_title": "Spam Scores"
38 | },
39 | "options_ui": {
40 | "page": "src/options/options.html",
41 | "open_in_tab": false,
42 | "browser_style": true
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/test/mail-examples/spam-status-6.eml:
--------------------------------------------------------------------------------
1 | To: receiver@spamscores
2 | From: sender@spamscores
3 | Subject: [X-Spam-Status] Spam Scores test mail (spam-status-6.eml)
4 | X-Spam-Status: Yes, score=-42.42 required=5.0 tests=R_DKIM_ALLOW,R_MIXED_CHARSET,
5 | FREEMAIL_FROM,DKIM_TRACE,MX_GOOD,BAYES_HAM autolearn=no version=3.3.2
6 | X-Spam-Report:
7 | * -0.2 R_DKIM_ALLOW DKIM verification succeed
8 | * 0.18 R_MIXED_CHARSET Mixed characters in a message
9 | * 0.00 FREEMAIL_FROM From is a Freemail address
10 | * [gmx.de]
11 | * -0.00 DKIM_TRACE DKIM checks completed
12 | * -0.01 MX_GOOD Domain has working MX
13 | * -5.5 BAYES_HAM Message probably ham
14 | X-Spam-Level:
15 | X-Spam-Checker-Version: SpamAssassin 3.3.2 (2011-06-06) on srv01.spamscores
16 | Message-ID: spam-status-6
17 |
18 | This is a message to test the Spam Scores Thunderbird add-on.
19 |
20 | --
21 | To: receiver@spamscores
22 | From: sender@spamscores
23 | Subject: [X-Spam-Status] Spam Scores test mail (spam-status-6.eml)
24 | X-Spam-Status: Yes, score=-42.42 required=5.0 tests=R_DKIM_ALLOW,R_MIXED_CHARSET,
25 | FREEMAIL_FROM,DKIM_TRACE,MX_GOOD,BAYES_HAM autolearn=no version=3.3.2
26 | X-Spam-Report:
27 | * -0.2 R_DKIM_ALLOW DKIM verification succeed
28 | * 0.18 R_MIXED_CHARSET Mixed characters in a message
29 | * 0.00 FREEMAIL_FROM From is a Freemail address
30 | * [gmx.de]
31 | * -0.00 DKIM_TRACE DKIM checks completed
32 | * -0.01 MX_GOOD Domain has working MX
33 | * -5.5 BAYES_HAM Message probably ham
34 | X-Spam-Level:
35 | X-Spam-Checker-Version: SpamAssassin 3.3.2 (2011-06-06) on srv01.spamscores
36 | Message-ID: spam-status-6
--------------------------------------------------------------------------------
/src/functions.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Functions module.
3 | * @module functions
4 | */
5 | import {
6 | DEFAULT_SCORE_LOWER_BOUNDS,
7 | DEFAULT_SCORE_UPPER_BOUNDS
8 | // MAX_SCORE_SEEN,
9 | // MIN_SCORE_SEEN,
10 | // SCORE_INTERPOLATION
11 | } from './constants.js'
12 |
13 | /**
14 | * Returns lower & upper bounds
15 | * @param {Object} storage Local storage with lower & upper bounds
16 | * @returns {number[]} Lower & upper bounds
17 | */
18 | export function getBounds(storage) {
19 | const lowerBounds = parseFloat(storage.scoreIconLowerBounds || DEFAULT_SCORE_LOWER_BOUNDS)
20 | const upperBounds = parseFloat(storage.scoreIconUpperBounds || DEFAULT_SCORE_UPPER_BOUNDS)
21 | return [lowerBounds, upperBounds]
22 | }
23 |
24 | // /**
25 | // *
26 | // * @param {string} headername
27 | // * @param {string} score
28 | // * @returns {string} Interpolated score or the raw score
29 | // */
30 | // export function scoreInterpolation(headername, score) {
31 | // const scoreType = SCORE_INTERPOLATION[headername]
32 | // if (scoreType) {
33 | // let numerator = 1
34 | // let denominator = 1
35 | // if (score > scoreType.MAX_UPPER_BOUNDS) {
36 | // numerator = MAX_SCORE_SEEN - DEFAULT_SCORE_UPPER_BOUNDS
37 | // denominator = scoreType.MAX_VALUE - scoreType.UPPER_BOUNDS
38 | // } else if (score < scoreType.MIN_LOWER_BOUNDS) {
39 | // numerator = MIN_SCORE_SEEN - DEFAULT_SCORE_LOWER_BOUNDS
40 | // denominator = scoreType.MIN_VALUE - scoreType.LOWER_BOUNDS
41 | // } else {
42 | // numerator = DEFAULT_SCORE_UPPER_BOUNDS - DEFAULT_SCORE_LOWER_BOUNDS
43 | // denominator = scoreType.UPPER_BOUNDS - scoreType.LOWER_BOUNDS
44 | // }
45 | // const scale = numerator / denominator
46 | // score *= scale
47 | // }
48 | // return score
49 | // }
50 |
--------------------------------------------------------------------------------
/src/browser/popup/popup.css:
--------------------------------------------------------------------------------
1 | body {
2 | padding: 0px;
3 | }
4 | h4 {
5 | margin: 0 4px 10px 4px;
6 | }
7 | h5#no-details {
8 | margin: 0;
9 | display: none;
10 | }
11 | table#score-details {
12 | border-collapse: collapse;
13 | margin: 0;
14 | padding: 0;
15 | padding-bottom: 5px;
16 | width: 100%;
17 | font-size: 14px;
18 | display: none;
19 | }
20 | table#score-details tr th {
21 | text-align: left;
22 | padding: 5px;
23 | }
24 | table#score-details tr td {
25 | vertical-align: top;
26 | padding: 5px;
27 | }
28 | table#score-details tr:last-child td {
29 | border-bottom: none;
30 | }
31 | table#score-details tr th:nth-child(1) {
32 | padding: 0 20px;
33 | }
34 | table#score-details tr td:nth-child(1) {
35 | text-align: center;
36 | padding: 5px 20px;
37 | }
38 | table#score-details tr td:nth-child(2),
39 | table#score-details tr td:nth-child(3) {
40 | padding: 7px 5px;
41 | }
42 | table#score-details tr td:nth-child(3) {
43 | max-width: 400px;
44 | }
45 | table#score-details tr.score-detail-row td div.info {
46 | word-break: break-all;
47 | }
48 | table#score-details tr.score-detail-row .score {
49 | display: inline-block;
50 | padding: 2px 4px;
51 | border-radius: 3px;
52 | color: white;
53 | width: 100%;
54 | }
55 | table#score-details tr.score-detail-row.negative .score {
56 | background-color: #28b62c;
57 | }
58 | table#score-details tr.score-detail-row.positive .score {
59 | background-color: #fe330a;
60 | }
61 | table#score-details tr.score-detail-row.neutral .score {
62 | background-color: #a1a1a1;
63 | }
64 |
65 | /* COLOR SETTINGS FOR DARK/LIGHT THEME */
66 | @media (prefers-color-scheme: light) {
67 | body {
68 | color: black;
69 | background: white;
70 | }
71 | table#score-details tr.score-detail-row td div.info {
72 | color: #7a7a7a;
73 | }
74 | table#score-details tr td {
75 | border-bottom: 1px solid #eee;
76 | }
77 | }
78 |
79 | @media (prefers-color-scheme: dark) {
80 | body {
81 | color: white;
82 | background: #2b2a33;
83 | }
84 | table#score-details tr.score-detail-row td div.info {
85 | color: #a0a0a0;
86 | }
87 | table#score-details tr td {
88 | border-bottom: 1px solid #464646;
89 | }
90 | table#score-details tr.score-detail-row.neutral .score {
91 | background-color: #6d6d6d;
92 | }
93 | }
94 |
--------------------------------------------------------------------------------
/src/options/options.css:
--------------------------------------------------------------------------------
1 | .input-line * {
2 | display: inline-block;
3 | vertical-align: middle;
4 | margin-top: 10px;
5 | font-size: 14px;
6 | }
7 | .input-line img {
8 | margin-right: 5px;
9 | }
10 | .checkbox-wrapper {
11 | line-height: 40px;
12 | font-size: 14px;
13 | margin-left: 30px;
14 | }
15 | .checkbox-wrapper * {
16 | display: inline-block;
17 | vertical-align: middle;
18 | }
19 |
20 | .sortable-list {
21 | border: 1px solid #ccc;
22 | border-radius: 4px;
23 | padding: 10px;
24 | max-width: 400px;
25 | min-height: 50px;
26 | }
27 |
28 | .sortable-item {
29 | background: #f5f5f5;
30 | border: 1px solid #ddd;
31 | padding: 8px 12px;
32 | margin: 5px 0;
33 | border-radius: 3px;
34 | cursor: move;
35 | user-select: none;
36 | display: flex;
37 | align-items: center;
38 | font-family: monospace;
39 | font-size: 13px;
40 | }
41 |
42 | .sortable-item:hover {
43 | background: #e8e8e8;
44 | }
45 |
46 | .sortable-item.dragging {
47 | opacity: 0.5;
48 | }
49 |
50 | .sortable-item::before {
51 | content: '⋮⋮';
52 | margin-right: 10px;
53 | color: #999;
54 | font-size: 16px;
55 | font-weight: bold;
56 | }
57 |
58 | .reset-button {
59 | padding: 6px 12px;
60 | background: #f0f0f0;
61 | border: 1px solid #ccc;
62 | border-radius: 3px;
63 | cursor: pointer;
64 | font-size: 13px;
65 | }
66 |
67 | .reset-button:hover {
68 | background: #e0e0e0;
69 | }
70 |
71 | .reset-button:active {
72 | background: #d0d0d0;
73 | }
74 |
75 | /* COLOR SETTINGS FOR DARK/LIGHT THEME */
76 | @media (prefers-color-scheme: light) {
77 | body {
78 | color: black;
79 | background: white;
80 | }
81 | }
82 |
83 | @media (prefers-color-scheme: dark) {
84 | body {
85 | color: white;
86 | background: #23222b;
87 | }
88 |
89 | .sortable-list {
90 | border-color: #555;
91 | }
92 |
93 | .sortable-item {
94 | background: #333;
95 | border-color: #555;
96 | color: white;
97 | }
98 |
99 | .sortable-item:hover {
100 | background: #444;
101 | }
102 |
103 | .reset-button {
104 | background: #333;
105 | border-color: #555;
106 | color: white;
107 | }
108 |
109 | .reset-button:hover {
110 | background: #444;
111 | }
112 |
113 | .reset-button:active {
114 | background: #555;
115 | }
116 | }
117 |
--------------------------------------------------------------------------------
/_locales/en/messages.json:
--------------------------------------------------------------------------------
1 | {
2 | "extensionName": {
3 | "message": "Spam Scores",
4 | "description": "Name of the add-on"
5 | },
6 | "extensionDescription": {
7 | "message": "Displays spam scores according to mail headers.",
8 | "description": "Description of the add-on"
9 | },
10 | "optionsIconRanges": {
11 | "message": "Score icon ranges"
12 | },
13 | "optionsScoreGreater": {
14 | "message": "Score geater than"
15 | },
16 | "optionsScoreBetween": {
17 | "message": "Score between $1 and $2 (both inclusive)"
18 | },
19 | "optionsScoreLess": {
20 | "message": "Score less than"
21 | },
22 | "optionsHideIconAndScore": {
23 | "message": "Hide icon and score in column"
24 | },
25 | "optionsHeaderPriority": {
26 | "message": "Header parsing priority"
27 | },
28 | "optionsHeaderPriorityDescription": {
29 | "message": "Set the order in which spam headers are parsed. The first matching header will be used. Drag to reorder."
30 | },
31 | "optionsScoreHeaders": {
32 | "message": "Score headers (used for score value)"
33 | },
34 | "optionsScoreDetailsHeaders": {
35 | "message": "Score details headers (used for score breakdown)"
36 | },
37 | "optionsResetToDefault": {
38 | "message": "Reset to default order"
39 | },
40 | "helloPopupTitle": {
41 | "message": "Spam Scores - Thank you!"
42 | },
43 | "helloHeadline": {
44 | "message": "Thank you for installing Spam Scores!"
45 | },
46 | "helloParagraph1": {
47 | "message": "In order to display the spam score column:"
48 | },
49 | "helloInstruction_1": {
50 | "message": "Restart Thunderbird."
51 | },
52 | "helloInstruction_2": {
53 | "message": "Right-click on a folder and select \"Properties\"."
54 | },
55 | "helloInstruction_3": {
56 | "message": "Click on \"Repair Folder\"."
57 | },
58 | "helloParagraph2": {
59 | "message": "This will scan the mail headers of all mails in this folder so that the spam score column can be displayed correctly."
60 | },
61 | "helloParagraph3": {
62 | "message": "Repair all folders like this in which you want to display the spam column."
63 | },
64 | "helloParagraph4": {
65 | "message": "Finally, you can enable the \"Spam score\" column in the list view of the repaired folder."
66 | },
67 | "helloFooterParagraph1": {
68 | "message": "If you have questions or encounter any bugs while using this add-on, please do not hesitate to create an issue for it in on GitHub (https://github.com/friedPotat0/Spam-Scores)."
69 | },
70 | "helloFooterParagraph2": {
71 | "message": "Please leave a review if you like this add-on. 😊"
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/_locales/es/messages.json:
--------------------------------------------------------------------------------
1 | {
2 | "extensionName": {
3 | "message": "Spam Scores",
4 | "description": "Nombre de la extensión"
5 | },
6 | "extensionDescription": {
7 | "message": "Muestra los puntos de Spam según las cabeceras.",
8 | "description": "Descripción de la extensión"
9 | },
10 | "optionsIconRanges": {
11 | "message": "Rangos de los iconos"
12 | },
13 | "optionsScoreGreater": {
14 | "message": "Puntuación mayor que"
15 | },
16 | "optionsScoreBetween": {
17 | "message": "Puntuación entre $1 y $2 (ambos incluidos)"
18 | },
19 | "optionsScoreLess": {
20 | "message": "Puntuación menor que"
21 | },
22 | "optionsHideIconAndScore": {
23 | "message": "Ocultar icono y puntuación en la columna"
24 | },
25 | "optionsHeaderPriority": {
26 | "message": "Prioridad de análisis de encabezados"
27 | },
28 | "optionsHeaderPriorityDescription": {
29 | "message": "Establezca el orden en que se analizan los encabezados de spam. Se utilizará el primer encabezado coincidente. Arrastre para reordenar."
30 | },
31 | "optionsScoreHeaders": {
32 | "message": "Encabezados de puntuación (usados para valor de puntuación)"
33 | },
34 | "optionsScoreDetailsHeaders": {
35 | "message": "Encabezados de detalles de puntuación (usados para desglose)"
36 | },
37 | "optionsResetToDefault": {
38 | "message": "Restablecer al orden predeterminado"
39 | },
40 | "helloPopupTitle": {
41 | "message": "Spam Scores - ¡Gracias!"
42 | },
43 | "helloHeadline": {
44 | "message": "¡Gracias por instalar Spam Scores!"
45 | },
46 | "helloParagraph1": {
47 | "message": "Para mostrar la columna de Spam:"
48 | },
49 | "helloInstruction_1": {
50 | "message": "Reinicia Thunderbird."
51 | },
52 | "helloInstruction_2": {
53 | "message": "Botón derecho en una carpeta > \"Propiedades\"."
54 | },
55 | "helloInstruction_3": {
56 | "message": "Click en \"Reparar Carpeta\"."
57 | },
58 | "helloParagraph2": {
59 | "message": "Ahora escaneará todas las cabeceras de los emails para que se muestre el \"Spam Score\"."
60 | },
61 | "helloParagraph3": {
62 | "message": "Repara las carpetas en los que te interese tener el \"Spam Score\"."
63 | },
64 | "helloParagraph4": {
65 | "message": "Finalmente, habilita la columna \"Spam score\" en la vista de la carpeta reparada."
66 | },
67 | "helloFooterParagraph1": {
68 | "message": "Si tienes dudas o encuentras algun fallo, por favor crea un issue en GitHub (https://github.com/friedPotat0/Spam-Scores)."
69 | },
70 | "helloFooterParagraph2": {
71 | "message": "Por favor deja una reseña si te gusta esta extensión. 😊"
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/_locales/de/messages.json:
--------------------------------------------------------------------------------
1 | {
2 | "extensionName": {
3 | "message": "Spam Scores",
4 | "description": "Name der Erweiterung"
5 | },
6 | "extensionDescription": {
7 | "message": "Zeigt Spam-Bewertungen anhand von E-Mail-Headern an.",
8 | "description": "Beschreibung der Erweiterung"
9 | },
10 | "optionsIconRanges": {
11 | "message": "Angezeigte Bewertungssymbole"
12 | },
13 | "optionsScoreGreater": {
14 | "message": "Punktzahl größer"
15 | },
16 | "optionsScoreBetween": {
17 | "message": "Punktzahl zwischen $1 und $2 (inklusiv)"
18 | },
19 | "optionsScoreLess": {
20 | "message": "Punktzahl kleiner"
21 | },
22 | "optionsHideIconAndScore": {
23 | "message": "Symbol und Punktzahl in der Spalte nicht anzeigen"
24 | },
25 | "optionsHeaderPriority": {
26 | "message": "Header-Parsing-Priorität"
27 | },
28 | "optionsHeaderPriorityDescription": {
29 | "message": "Legen Sie die Reihenfolge fest, in der Spam-Header analysiert werden. Der erste übereinstimmende Header wird verwendet. Ziehen Sie zum Neuordnen."
30 | },
31 | "optionsScoreHeaders": {
32 | "message": "Score-Header (für Gesamtpunktzahl verwendet)"
33 | },
34 | "optionsScoreDetailsHeaders": {
35 | "message": "Score-Details-Header (für Spam-Details verwendet)"
36 | },
37 | "optionsResetToDefault": {
38 | "message": "Auf Standardreihenfolge zurücksetzen"
39 | },
40 | "helloPopupTitle": {
41 | "message": "Spam Scores - Danke!"
42 | },
43 | "helloHeadline": {
44 | "message": "Vielen Dank, dass Sie Spam Scores installiert haben!"
45 | },
46 | "helloParagraph1": {
47 | "message": "Um die Spam-Score-Spalte anzuzeigen:"
48 | },
49 | "helloInstruction_1": {
50 | "message": "Müssen Sie zunächst Thunderbird neu starten."
51 | },
52 | "helloInstruction_2": {
53 | "message": "Klicken Sie anschließend mit der rechten Maustaste auf einen Ordner und wählen Sie \"Eigenschaften\"."
54 | },
55 | "helloInstruction_3": {
56 | "message": "Klicken Sie dann auf \"Reparieren\"."
57 | },
58 | "helloParagraph2": {
59 | "message": "Dadurch werden die Mail-Header aller Mails in diesem Ordner gescannt, damit die Spam-Score-Spalte korrekt angezeigt werden kann."
60 | },
61 | "helloParagraph3": {
62 | "message": "Reparieren Sie alle Ordner, in denen Sie die Spalte anzeigen möchten."
63 | },
64 | "helloParagraph4": {
65 | "message": "Anschließend können Sie die Spalte \"Spam score\" in der Listenansicht des reparierten Ordners aktivieren."
66 | },
67 | "helloFooterParagraph1": {
68 | "message": "Bei Fragen oder Fehlern, zögern Sie bitte nicht, ein Issue dafür auf GitHub zu erstellen (https://github.com/friedPotat0/Spam-Scores)."
69 | },
70 | "helloFooterParagraph2": {
71 | "message": "Wenn Ihnen das Add-on gefällt, würde ich mich über eine Bewertung sehr freuen. 😊"
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/_locales/it/messages.json:
--------------------------------------------------------------------------------
1 | {
2 | "extensionName": {
3 | "message": "Spam Scores",
4 | "description": "Name of the add-on"
5 | },
6 | "extensionDescription": {
7 | "message": "Visualizza i punteggi spam in base alle intestazioni delle email.",
8 | "description": "Description of the add-on"
9 | },
10 | "optionsIconRanges": {
11 | "message": "Intervalli delle icone di punteggio"
12 | },
13 | "optionsScoreGreater": {
14 | "message": "Punteggio maggiore di"
15 | },
16 | "optionsScoreBetween": {
17 | "message": "Punteggio tra $1 e $2 (inclusi entrambi)"
18 | },
19 | "optionsScoreLess": {
20 | "message": "Punteggio inferiore a"
21 | },
22 | "optionsHideIconAndScore": {
23 | "message": "Nascondi icona e punteggio nella colonna"
24 | },
25 | "optionsHeaderPriority": {
26 | "message": "Priorità di analisi degli header"
27 | },
28 | "optionsHeaderPriorityDescription": {
29 | "message": "Imposta l'ordine in cui vengono analizzati gli header spam. Verrà utilizzato il primo header corrispondente. Trascina per riordinare."
30 | },
31 | "optionsScoreHeaders": {
32 | "message": "Header di punteggio (utilizzati per il valore del punteggio)"
33 | },
34 | "optionsScoreDetailsHeaders": {
35 | "message": "Header di dettagli del punteggio (utilizzati per la suddivisione)"
36 | },
37 | "optionsResetToDefault": {
38 | "message": "Ripristina l'ordine predefinito"
39 | },
40 | "helloPopupTitle": {
41 | "message": "Spam Scores - Grazie!"
42 | },
43 | "helloHeadline": {
44 | "message": "Grazie per aver installato Spam Scores!"
45 | },
46 | "helloParagraph1": {
47 | "message": "Per visualizzare la colonna del punteggio spam:"
48 | },
49 | "helloInstruction_1": {
50 | "message": "Riavvia Thunderbird."
51 | },
52 | "helloInstruction_2": {
53 | "message": "Fai clic destro su una cartella e seleziona \"Proprietà\"."
54 | },
55 | "helloInstruction_3": {
56 | "message": "Clicca su \"Ripara cartella\"."
57 | },
58 | "helloParagraph2": {
59 | "message": "Questo scansionerà le intestazioni delle email in questa cartella in modo che la colonna del punteggio spam possa essere visualizzata correttamente."
60 | },
61 | "helloParagraph3": {
62 | "message": "Ripara tutte le cartelle in cui desideri visualizzare la colonna spam in questo modo."
63 | },
64 | "helloParagraph4": {
65 | "message": "Infine, puoi abilitare la colonna \"Spam Scores\" nella visualizzazione elenco della cartella riparata."
66 | },
67 | "helloFooterParagraph1": {
68 | "message": "Se hai domande o riscontri errori durante l'utilizzo di questo componente aggiuntivo, non esitare a creare una issue su GitHub (https://github.com/friedPotat0/Spam-Scores)."
69 | },
70 | "helloFooterParagraph2": {
71 | "message": "Lascia una recensione se ti piace questo componente aggiuntivo. 😊"
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/src/options/options.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | options.html
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
65 |
66 |
67 |
68 |
74 |
75 |
76 |
--------------------------------------------------------------------------------
/src/experiments/schema.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "namespace": "SpamScores",
4 | "functions": [
5 | {
6 | "name": "setScoreBounds",
7 | "type": "function",
8 | "description": "",
9 | "async": false,
10 | "parameters": [
11 | {
12 | "name": "lower",
13 | "type": "number"
14 | },
15 | {
16 | "name": "upper",
17 | "type": "number"
18 | }
19 | ]
20 | },
21 | {
22 | "name": "setHideIconScoreOptions",
23 | "type": "function",
24 | "description": "",
25 | "async": false,
26 | "parameters": [
27 | {
28 | "name": "hidePositive",
29 | "type": "boolean"
30 | },
31 | {
32 | "name": "hideNeutral",
33 | "type": "boolean"
34 | },
35 | {
36 | "name": "hideNegative",
37 | "type": "boolean"
38 | }
39 | ]
40 | },
41 | {
42 | "name": "getHelloFlag",
43 | "type": "function",
44 | "description": "",
45 | "async": true,
46 | "parameters": []
47 | },
48 | {
49 | "name": "setCustomMailscannerHeaders",
50 | "type": "function",
51 | "description": "",
52 | "async": false,
53 | "parameters": [
54 | {
55 | "name": "customMailscannerHeaders",
56 | "type": "array",
57 | "items": {
58 | "type": "string"
59 | }
60 | }
61 | ]
62 | },
63 | {
64 | "name": "setScoreHeaderOrder",
65 | "type": "function",
66 | "description": "Set the order in which score headers are parsed",
67 | "async": false,
68 | "parameters": [
69 | {
70 | "name": "scoreHeaderOrder",
71 | "type": "array",
72 | "items": {
73 | "type": "string"
74 | }
75 | }
76 | ]
77 | },
78 | {
79 | "name": "setScoreDetailsHeaderOrder",
80 | "type": "function",
81 | "description": "Set the order in which score details headers are parsed",
82 | "async": false,
83 | "parameters": [
84 | {
85 | "name": "scoreDetailsHeaderOrder",
86 | "type": "array",
87 | "items": {
88 | "type": "string"
89 | }
90 | }
91 | ]
92 | },
93 | {
94 | "name": "addHeadersToPrefs",
95 | "type": "function",
96 | "description": "",
97 | "async": true,
98 | "parameters": [
99 | {
100 | "name": "dynamicHeaders",
101 | "type": "array",
102 | "optional": true,
103 | "items": {
104 | "type": "string"
105 | }
106 | }
107 | ]
108 | },
109 | {
110 | "name": "addColumns",
111 | "type": "function",
112 | "description": "",
113 | "async": true,
114 | "parameters": [
115 | {
116 | "name": "nameSpamCoreValue",
117 | "type": "string"
118 | },
119 | {
120 | "name": "nameSpamCoreIcon",
121 | "type": "string"
122 | }
123 | ]
124 | },
125 | {
126 | "name": "removeColumns",
127 | "type": "function",
128 | "description": "",
129 | "async": true,
130 | "parameters": []
131 | }
132 | ]
133 | }
134 | ]
135 |
--------------------------------------------------------------------------------
/src/constants.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Constants module
3 | * @module constants
4 | * @see module:functions
5 | */
6 |
7 | /**
8 | * A score lower than this is considered a good email
9 | * @constant {number}
10 | */
11 | export const DEFAULT_SCORE_LOWER_BOUNDS = -2.0
12 |
13 | /**
14 | * A score higher than this is considered a bad email
15 | * @constant {number}
16 | */
17 | export const DEFAULT_SCORE_UPPER_BOUNDS = 2.0
18 |
19 | // /**
20 | // * Minimum score for SCORE_INTERPOLATION
21 | // * @constant {number} Minimum score seen from the first Score Domain that was made for (Rspamd score)
22 | // * @see module:functions.scoreInterpolation
23 | // */
24 | // export const MIN_SCORE_SEEN = -40
25 |
26 | // /**
27 | // * Maximum score for SCORE_INTERPOLATION
28 | // * @constant {number} Maximum score seen from the first Score Domain that was made for (Rspamd score)
29 | // * @see module:functions.scoreInterpolation
30 | // */
31 | // export const MAX_SCORE_SEEN = 40
32 |
33 | // /**
34 | // * @constant {Object}
35 | // * @type {Object}
36 | // */
37 | // export const SCORE_INTERPOLATION = {
38 | // 'x-vr-spamscore': { MIN_VALUE: 0, MAX_VALUE: 900, LOWER_BOUNDS: 100, UPPER_BOUNDS: 300 }
39 | // }
40 |
41 | /**
42 | * @constant {Object}
43 | */
44 | export const SCORE_REGEX = {
45 | 'x-spamd-result': /\[([-+]?[0-9]+\.?[0-9]*) \/ [-+]?[0-9]+\.?[0-9]*\];/,
46 | 'x-spam-status': /(?:Yes|No)(?:, score=|\/)([-+]?[0-9]+\.?[0-9]*)/,
47 | 'x-rspam-status': /(?:Yes|No)(?:, score=|\/)([-+]?[0-9]+\.?[0-9]*)/,
48 | 'x-spam-score': /([-+]?[0-9]+\.?[0-9]*)/,
49 | 'x-spam-report': /([-+]?[0-9]+\.?[0-9]*) hits,/,
50 | 'x-ham-report': /([-+]?[0-9]+\.?[0-9]*) hits,/,
51 | 'x-rspamd-score': /([-+]?[0-9]+\.?[0-9]*)/,
52 | 'x-vr-spamscore': /([0-9]+)/,
53 | 'x-hmailserver-reason-score': /([-+]?[0-9]+\.?[0-9]*)/
54 | }
55 |
56 | /**
57 | * For customised headers
58 | * @constant {Object}
59 | */
60 | export const CUSTOM_SCORE_REGEX = {
61 | 'mailscanner-spamcheck':
62 | /(?:score|punteggio|puntuació|sgor\/score|skore|Wertung|bedømmelse|puntaje|pont|escore|resultat|skore)=([-+]?[0-9]+\.?[0-9]*),/
63 | }
64 |
65 | /**
66 | * Regex for hMailServer reason headers
67 | * @constant {RegExp}
68 | */
69 | export const HMAILSERVER_REASON_REGEX = /^x-hmailserver-reason-\d+$/
70 |
71 | /**
72 | * Headers that contain the score details
73 | * @constant {String[]}
74 | */
75 | export const SCORE_DETAILS_ARRAY = [
76 | 'x-spamd-result',
77 | 'x-spam-report',
78 | 'x-ham-report',
79 | 'x-spamcheck',
80 | 'x-spam-status',
81 | 'x-rspamd-report',
82 | 'x-hmailserver-reason-score'
83 | ]
84 |
85 | /**
86 | * Default order for parsing score headers (first match is used)
87 | * @constant {String[]}
88 | */
89 | export const DEFAULT_SCORE_HEADER_ORDER = [
90 | 'x-spamd-result',
91 | 'x-spam-status',
92 | 'x-rspam-status',
93 | 'x-spam-score',
94 | 'x-spam-report',
95 | 'x-ham-report',
96 | 'x-rspamd-score',
97 | 'x-vr-spamscore',
98 | 'x-hmailserver-reason-score'
99 | ]
100 |
101 | /**
102 | * Default order for parsing score details headers (first match is used)
103 | * @constant {String[]}
104 | */
105 | export const DEFAULT_SCORE_DETAILS_ORDER = [
106 | 'x-spamd-result',
107 | 'x-spam-report',
108 | 'x-ham-report',
109 | 'x-spam-status',
110 | 'x-rspamd-report',
111 | 'x-hmailserver-reason-score'
112 | ]
113 |
114 | /** @constant {Object} */
115 | export const SYMBOL_REGEX = {
116 | prefix: /\*? +-?[\d.]+[ \)=]+(?:[A-Z][A-Z0-9_]+|--) .*?(?=\*? +-?[\d.]+[ \)=]+(?:[A-Z][A-Z0-9_]+|--) |$)/gs,
117 | prefixSingle: /(?:\* +)?(-?[\d.]+)[ \)=]+(?:([A-Z][A-Z0-9_]+)|--) ([\s\S]*?)(?:\[(.*)\])?$/,
118 | suffix: /([A-Z][A-Z0-9_]+)(?:(?:[ \(=](-?[\d.]+)\)?(?:\[(.*?)\])?)|, *| |\r?\n|$)/g
119 | }
120 |
--------------------------------------------------------------------------------
/test/mail-examples/spam-status-8.eml:
--------------------------------------------------------------------------------
1 | To: receiver@spamscores
2 | From: sender@spamscores
3 | Subject: [X-Spam-Status] Spam Scores test mail (spam-status-8.eml)
4 | X-Spam-Subject: [X-Spam-Status] Spam Scores test mail (spam-status-8.eml)
5 | X-Spam-Status: Yes, score=-42.42
6 | X-Spam-Score: -4242
7 | X-Spam-Bar: +++++++++++++++++++++
8 | X-Spam-Report: Spam detection software, running on the system "*****",
9 | has identified this incoming email as possible spam. The original
10 | message has been attached to this so you can view it or label
11 | similar future email. If you have any questions, see
12 | root\@localhost for details.
13 | Content preview: This is a message to test the Spam Scores Thunderbird add-on. [...]
14 | Content analysis details: (21.2 points, 5.0 required)
15 | pts rule name description
16 | ---- ---------------------- --------------------------------------------------
17 | 4.5 URIBL_DBL_SPAM Contains a spam URL listed in the Spamhaus DBL
18 | blocklist
19 | [URIs: topdelivery.net.pl]
20 | 5.0 URIBL_BLACK Contains an URL listed in the URIBL blacklist
21 | [URIs: topdelivery.net.pl]
22 | 1.0 BAYES_999 BODY: Bayes spam probability is 99.9 to 100%
23 | [score: 1.0000]
24 | 5.0 BAYES_99 BODY: Bayes spam probability is 99 to 100%
25 | [score: 1.0000]
26 | 0.2 HEADER_FROM_DIFFERENT_DOMAINS From and EnvelopeFrom 2nd level
27 | mail domains are different
28 | -0.0 SPF_PASS SPF: sender matches SPF record
29 | 0.0 HTML_MESSAGE BODY: HTML included in message
30 | 0.0 HTML_IMAGE_RATIO_02 BODY: HTML has a low ratio of text to image
31 | area
32 | 0.5 KAM_REALLYHUGEIMGSRC RAW: Spam with image tags with ridiculously
33 | huge http urls
34 | -0.1 DKIM_VALID Message has at least one valid DKIM or DK signature
35 | 0.1 DKIM_SIGNED Message has a DKIM or DK signature, not necessarily
36 | valid
37 | -0.1 DKIM_VALID_EF Message has a valid DKIM or DK signature from
38 | envelope-from domain
39 | 5.0 KAM_VERY_BLACK_DBL Email that hits both URIBL Black and Spamhaus
40 | DBL
41 | X-Spam-Flag: YES
42 |
43 | This is a message to test the Spam Scores Thunderbird add-on.
44 |
45 | --
46 | To: receiver@spamscores
47 | From: sender@spamscores
48 | Subject: [X-Spam-Status] Spam Scores test mail (spam-status-8.eml)
49 | X-Spam-Subject: [X-Spam-Status] Spam Scores test mail (spam-status-8.eml)
50 | X-Spam-Status: Yes, score=-42.42
51 | X-Spam-Score: -4242
52 | X-Spam-Bar: +++++++++++++++++++++
53 | X-Spam-Report: Spam detection software, running on the system "*****",
54 | has identified this incoming email as possible spam. The original
55 | message has been attached to this so you can view it or label
56 | similar future email. If you have any questions, see
57 | root\@localhost for details.
58 | Content preview: This is a message to test the Spam Scores Thunderbird add-on. [...]
59 | Content analysis details: (21.2 points, 5.0 required)
60 | pts rule name description
61 | ---- ---------------------- --------------------------------------------------
62 | 4.5 URIBL_DBL_SPAM Contains a spam URL listed in the Spamhaus DBL
63 | blocklist
64 | [URIs: topdelivery.net.pl]
65 | 5.0 URIBL_BLACK Contains an URL listed in the URIBL blacklist
66 | [URIs: topdelivery.net.pl]
67 | 1.0 BAYES_999 BODY: Bayes spam probability is 99.9 to 100%
68 | [score: 1.0000]
69 | 5.0 BAYES_99 BODY: Bayes spam probability is 99 to 100%
70 | [score: 1.0000]
71 | 0.2 HEADER_FROM_DIFFERENT_DOMAINS From and EnvelopeFrom 2nd level
72 | mail domains are different
73 | -0.0 SPF_PASS SPF: sender matches SPF record
74 | 0.0 HTML_MESSAGE BODY: HTML included in message
75 | 0.0 HTML_IMAGE_RATIO_02 BODY: HTML has a low ratio of text to image
76 | area
77 | 0.5 KAM_REALLYHUGEIMGSRC RAW: Spam with image tags with ridiculously
78 | huge http urls
79 | -0.1 DKIM_VALID Message has at least one valid DKIM or DK signature
80 | 0.1 DKIM_SIGNED Message has a DKIM or DK signature, not necessarily
81 | valid
82 | -0.1 DKIM_VALID_EF Message has a valid DKIM or DK signature from
83 | envelope-from domain
84 | 5.0 KAM_VERY_BLACK_DBL Email that hits both URIBL Black and Spamhaus
85 | DBL
86 | X-Spam-Flag: YES
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Spam Scores (Thunderbird Add-on)
2 |
3 | Spam Scores is an add-on for Thunderbird (Version 91.0a1 - \*). For Thunderbird 78.* the last working version of this add-on is [1.3.1](https://github.com/friedPotat0/Spam-Scores/releases/tag/1.3.1).
4 |
5 | The add-on can display spam scores according to mail headers. The add-on supports spam/ham score headers of Rspamd, SpamAssassin, MailScanner and hMailServer. It adds a column with the overall spam score to the mail list view and shows details of any matched spam/ham rule.
6 |
7 | > :warning: The add-on needs mails with headers like "X-Spamd-Result", "X-Spam-Report", "X-Ham-Report", "X-Rspamd-Report/-Score", "X-SpamCheck", "X-Spam-Status", "X-Rspam-Status", "X-hMailServer-Reason-Score" or "X-VR-SPAMSCORE" to work. If a mail does not have one of these headers, it cannot display any spam score. Please make sure to check your mails for these headers before creating an issue. The add-on does not currently support spam headers from GMX, as these have a different score scale.
8 |
9 | 
10 |
11 | To display the spam score column, right-click on the title bar of the columns in the list view and select "Spam score". If the column is empty, you must first restart Thunderbird and then right-click on any folder and select "Properties" and "Repair Folder". This will scan the mail headers of all mails in this folder so that the spam score column can be displayed correctly. Repair all folders like this in which you want to display this column.
12 |
13 | If you have mails with the header "X-MYCOMPANY-MailScanner-SpamCheck", you have to open one of these mails first and then restart Thunderbird and repair the folder. Otherwise the spam score of the mails containing these headers will not be displayed.
14 |
15 | The total score of each mail with an existing spam header will be displayed along with a red, yellow or green icon depending on the score. The colours are by default calculated as follows:
16 |
17 | -  Score greater than 2
18 | -  Score between -2 and 2 (both inclusive)
19 | -  Score less than -2
20 |
21 | The icon score ranges can be changed in the [add-on options](#options).
22 |
23 | Furthermore, a button is displayed in the action bar of any opened mail with the respective total score of the mail. Clicking on the button opens a popup with detailed information on all individual rules. In addition to the name and the partial score, a description and, if available in the mail header, the value on the basis of which the score was calculated is displayed.
24 |
25 | ## Installation
26 |
27 | You can download the latested version reviewed by moz://a directly on the [Thunderbird Add-on page](https://addons.thunderbird.net/de/thunderbird/addon/spam-scores/) or through your installed Thunderbird client by clicking on the menu button followed by "Add-ons" and typing "Spam Scores" in the search bar.
28 |
29 | Additionally the latest reviewed version is available on the [Releases page](https://github.com/friedPotat0/Spam-Scores/releases) of this GitHub repository.
30 |
31 | To test versions that have not yet been published, you can always download the files from any branch and create a new ZIP file containing all files in the "Spam-Scores-[BRANCH_NAME]" folder. Then you can add the file to thunderbird by drag & drop to install the new version. Please keep in mind that you might not receive future updates until you reinstall a reviewed version directly from Thunderbird's add-on page or through the releases page of this repository.
32 |
33 | ## Options
34 |
35 | 
36 |
37 | The default icon ranges can be changed in the add-on settings in Thunderbird. Furthermore individual score ranges can be completely hidden. For example, this can be used to show the icon and score only for mails that are spam. The detailed rules of any opened mail can still be accessed regardless of this setting.
38 |
39 | ## Translations
40 |
41 | At the moment the add-on is mostly written in English. Some parts like the settings and the description are also translated to German. Please refer to the section [Contributing](#contributing) if you would like to help by translating the add-on to different languages.
42 |
43 | ## License
44 |
45 | The add-on is released under the CC BY-NC-SA 4.0 (Attribution-NonCommercial-ShareAlike 4.0 International) license.
46 |
47 | ## Contributing
48 |
49 | If you notice any bugs, do not hesitate to open an issue about it. Please understand that I develop the add-on in my spare time and may not be able to solve problems directly. If you want to contribute to the project by fixing bugs, implementing new features or translating the add-on, please feel free to open a pull request.
50 |
--------------------------------------------------------------------------------
/test/mail-examples/spam-report-1.eml:
--------------------------------------------------------------------------------
1 | To: receiver@spamscores
2 | From: sender@spamscores
3 | Subject: [X-Spam-Report] Spam Scores test mail (spam-report-1.eml)
4 | X-Spam-Subject: ***SPAM*** =?utf-8?B?TmllIHByemVnYXAhIE5vd2/Fm2NpIC0yNSUgeiBrb2RlbSBGUkVTSDI1IQ==?=
5 | X-Spam-Status: Yes, score=-42.42
6 | X-Spam-Score: -4242
7 | X-Spam-Bar: +++++++++++++
8 | X-Spam-Report: Spam detection software, running on the system "xxxxx",
9 | has identified this incoming email as possible spam. The original
10 | message has been attached to this so you can view it or label
11 | similar future email. If you have any questions, see
12 | root\@localhost for details.
13 | Content preview: SprawdĹşsporstylestory_mailing-sale DARMOWA DOSTAWA OD 200
14 | PLN Â Â / Â Â 30 DNI NA ZWROT Â Â / Â Â BEZPIECZNE ZAKUPY Â Â / Â Â
15 | RATY 0%Kampania realizowana przez Redgroup do bazy partnera Kampani [...]
16 |
17 | Content analysis details: (13.3 points, 4.0 required)
18 | pts rule name description
19 | ---- ---------------------- --------------------------------------------------
20 | 4.5 URIBL_DBL_SPAM Contains a spam URL listed in the Spamhaus DBL
21 | blocklist
22 | [URIs: topdelivery.net.pl]
23 | -1.9 BAYES_00 BODY: Bayes spam probability is 0 to 1%
24 | [score: 0.0000]
25 | 5.0 URIBL_BLACK Contains an URL listed in the URIBL blacklist
26 | [URIs: topdelivery.net.pl]
27 | -0.0 SPF_PASS SPF: sender matches SPF record
28 | 0.2 HEADER_FROM_DIFFERENT_DOMAINS From and EnvelopeFrom 2nd level
29 | mail domains are different
30 | 0.0 HTML_FONT_LOW_CONTRAST BODY: HTML font color similar or
31 | identical to background
32 | 0.0 HTML_IMAGE_RATIO_02 BODY: HTML has a low ratio of text to image
33 | area
34 | 0.0 HTML_MESSAGE BODY: HTML included in message
35 | 0.5 KAM_REALLYHUGEIMGSRC RAW: Spam with image tags with ridiculously
36 | huge http urls
37 | -0.1 DKIM_VALID Message has at least one valid DKIM or DK signature
38 | 0.1 DKIM_SIGNED Message has a DKIM or DK signature, not necessarily
39 | valid
40 | -0.1 DKIM_VALID_EF Message has a valid DKIM or DK signature from
41 | envelope-from domain
42 | 5.0 KAM_VERY_BLACK_DBL Email that hits both URIBL Black and Spamhaus
43 | DBL
44 | X-Spam-Flag: YES
45 |
46 | This is a message to test the Spam Scores Thunderbird add-on.
47 |
48 | --
49 | To: receiver@spamscores
50 | From: sender@spamscores
51 | Subject: [X-Spam-Report] Spam Scores test mail (spam-report-1.eml)
52 | X-Spam-Subject: ***SPAM*** =?utf-8?B?TmllIHByemVnYXAhIE5vd2/Fm2NpIC0yNSUgeiBrb2RlbSBGUkVTSDI1IQ==?=
53 | X-Spam-Status: Yes, score=-42.42
54 | X-Spam-Score: -4242
55 | X-Spam-Bar: +++++++++++++
56 | X-Spam-Report: Spam detection software, running on the system "xxxxx",
57 | has identified this incoming email as possible spam. The original
58 | message has been attached to this so you can view it or label
59 | similar future email. If you have any questions, see
60 | root\@localhost for details.
61 | Content preview: SprawdĹşsporstylestory_mailing-sale DARMOWA DOSTAWA OD 200
62 | PLN Â Â / Â Â 30 DNI NA ZWROT Â Â / Â Â BEZPIECZNE ZAKUPY Â Â / Â Â
63 | RATY 0%Kampania realizowana przez Redgroup do bazy partnera Kampani [...]
64 |
65 | Content analysis details: (13.3 points, 4.0 required)
66 | pts rule name description
67 | ---- ---------------------- --------------------------------------------------
68 | 4.5 URIBL_DBL_SPAM Contains a spam URL listed in the Spamhaus DBL
69 | blocklist
70 | [URIs: topdelivery.net.pl]
71 | -1.9 BAYES_00 BODY: Bayes spam probability is 0 to 1%
72 | [score: 0.0000]
73 | 5.0 URIBL_BLACK Contains an URL listed in the URIBL blacklist
74 | [URIs: topdelivery.net.pl]
75 | -0.0 SPF_PASS SPF: sender matches SPF record
76 | 0.2 HEADER_FROM_DIFFERENT_DOMAINS From and EnvelopeFrom 2nd level
77 | mail domains are different
78 | 0.0 HTML_FONT_LOW_CONTRAST BODY: HTML font color similar or
79 | identical to background
80 | 0.0 HTML_IMAGE_RATIO_02 BODY: HTML has a low ratio of text to image
81 | area
82 | 0.0 HTML_MESSAGE BODY: HTML included in message
83 | 0.5 KAM_REALLYHUGEIMGSRC RAW: Spam with image tags with ridiculously
84 | huge http urls
85 | -0.1 DKIM_VALID Message has at least one valid DKIM or DK signature
86 | 0.1 DKIM_SIGNED Message has a DKIM or DK signature, not necessarily
87 | valid
88 | -0.1 DKIM_VALID_EF Message has a valid DKIM or DK signature from
89 | envelope-from domain
90 | 5.0 KAM_VERY_BLACK_DBL Email that hits both URIBL Black and Spamhaus
91 | DBL
92 | X-Spam-Flag: YES
--------------------------------------------------------------------------------
/src/background/background.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 | import { SCORE_REGEX, CUSTOM_SCORE_REGEX, DEFAULT_SCORE_HEADER_ORDER } from '../constants.js'
3 | import { getBounds /* , scoreInterpolation */ } from '../functions.js'
4 |
5 | /**
6 | * @type {StorageArea}
7 | */
8 | const localStorage = messenger.storage.local
9 |
10 | /**
11 | * Functions
12 | */
13 |
14 | /**
15 | * @param {object} headers
16 | * @param {string[]} headerOrder - Custom order for parsing headers
17 | * @returns {string[]} Score value
18 | */
19 | function getScores(headers, headerOrder = null) {
20 | const scores = []
21 | // Get Custom Mail Headers
22 | const auxHeaders = Object.entries(headers).filter(([key, value]) => key.startsWith('x-'))
23 | // Remove Mozilla Headers
24 | const auxHeadersNoMozilla = auxHeaders.filter(([key, value]) => !key.startsWith('x-mozilla'))
25 | const customHeaders = Object.fromEntries(auxHeadersNoMozilla)
26 |
27 | // Use custom order if provided, otherwise use default order
28 | const scoreHeaders = headerOrder || DEFAULT_SCORE_HEADER_ORDER
29 |
30 | for (const headerName of scoreHeaders) {
31 | if (customHeaders[headerName]) {
32 | if (SCORE_REGEX[headerName]) {
33 | const scoreField = customHeaders[headerName][0].match(SCORE_REGEX[headerName])
34 | if (!scoreField) continue // If no match iterate
35 | // const score = scoreInterpolation(headerName, scoreField[1])
36 | const score = scoreField[1]
37 | scores.push(score)
38 | }
39 | }
40 | }
41 |
42 | // Check custom headers (e.g., mailscanner) if no score found yet
43 | if (scores.length === 0) {
44 | for (const headerName in customHeaders) {
45 | for (const regExName in CUSTOM_SCORE_REGEX) {
46 | if (headerName.endsWith(regExName)) {
47 | const scoreField = customHeaders[headerName][0].match(CUSTOM_SCORE_REGEX[regExName])
48 | if (!scoreField) continue // If no match iterate
49 | // const score = scoreInterpolation(headerName, scoreField[1])
50 | const score = scoreField[1]
51 | scores.push(score)
52 | }
53 | }
54 | }
55 | }
56 |
57 | return scores
58 | }
59 |
60 | /**
61 | * Returns the path of the image
62 | * @param {string} score
63 | * @returns {string} Path of Image
64 | */
65 | async function getImageSrc(score) {
66 | if (score === null) return '/images/score_no.svg'
67 | const storage = await localStorage.get(['scoreIconLowerBounds', 'scoreIconUpperBounds'])
68 | const [lowerBounds, upperBounds] = getBounds(storage)
69 | if (score > upperBounds) return '/images/score_positive.svg'
70 | if (score <= upperBounds && score >= lowerBounds) return '/images/score_neutral.svg'
71 | if (score < lowerBounds) return '/images/score_negative.svg'
72 | return '/images/score_neutral.svg'
73 | }
74 |
75 | /**
76 | * Executed everytime a message is displayed
77 | * @param {Tab} tab
78 | * @param {MessageHeader} message
79 | */
80 | async function onMessageDisplayed(tab, message) {
81 | // Declaration / Values
82 | const idTab = tab.id
83 | const fullMessage = await messenger.messages.getFull(message.id)
84 | const messageButton = messenger.messageDisplayAction
85 |
86 | // Get custom header order
87 | const storage = await localStorage.get(['scoreHeaderOrder'])
88 | const headerOrder = storage.scoreHeaderOrder || DEFAULT_SCORE_HEADER_ORDER
89 |
90 | // Get Score
91 | const scores = getScores(fullMessage.headers, headerOrder) // Get Scores
92 | const score = isNaN(scores[0]) ? null : scores[0]
93 |
94 | // Message Score Button
95 | if (score === null) {
96 | messageButton.disable(idTab)
97 | messageButton.setTitle({ tabId: idTab, title: 'No Spam Score' })
98 | messageButton.setIcon({ path: await getImageSrc(null) })
99 | } else {
100 | messageButton.enable(idTab)
101 | messageButton.setTitle({ tabId: idTab, title: 'Spam Score: ' + score })
102 | messageButton.setIcon({ path: await getImageSrc(score) })
103 | }
104 |
105 | /**
106 | * Save static (e.g. x-spam-score, x-rspamd-score) and dynamic (X--MailScanner-SpamCheck) header names
107 | * in global preferences to be stored by Thunderbird for each mail
108 | *
109 | * Reason to restart Thunderbird & repair folders
110 | */
111 | const dynamicHeaders = []
112 | let dynamicHeaderFound = false
113 | for (const dynamicHeaderSuffix in CUSTOM_SCORE_REGEX) {
114 | const fullDynamicHeaderName = Object.keys(fullMessage.headers).find(key => key.endsWith(dynamicHeaderSuffix))
115 | if (fullDynamicHeaderName) {
116 | dynamicHeaders.push(fullDynamicHeaderName)
117 | dynamicHeaderFound = true
118 | }
119 | }
120 | // Static header names will be automatically added
121 | await messenger.SpamScores.addHeadersToPrefs(dynamicHeaders)
122 | // Store new dynamic header names in localStorage to be recognised by the score column
123 | if (dynamicHeaderFound) {
124 | const storage = await localStorage.get(['customMailscannerHeaders'])
125 | let customMailscannerHeaders = storage.customMailscannerHeaders || []
126 | for (const header of dynamicHeaders) {
127 | if (!customMailscannerHeaders.includes(header)) {
128 | customMailscannerHeaders.push(header)
129 | }
130 | }
131 | localStorage.set({ customMailscannerHeaders })
132 | }
133 | }
134 |
135 | /**
136 | * Main
137 | */
138 | const init = async () => {
139 | // Declaration / Values
140 | const spamScores = messenger.SpamScores
141 | const storage = await localStorage.get([
142 | 'scoreIconLowerBounds',
143 | 'scoreIconUpperBounds',
144 | 'customMailscannerHeaders',
145 | 'hideIconScorePositive',
146 | 'hideIconScoreNeutral',
147 | 'hideIconScoreNegative',
148 | 'scoreHeaderOrder',
149 | 'scoreDetailsHeaderOrder',
150 | 'hello'
151 | ])
152 |
153 | // Hello Message
154 | if (!storage.hello) {
155 | /**
156 | * Additional condition deprecated (Fallback for add-on version <= 1.3.1)
157 | * Prevents displaying the message on subsequent installations.
158 | * Should be removed when majority has updated!
159 | */
160 | if (!(await spamScores.getHelloFlag())) {
161 | messenger.windows.create({
162 | height: 680,
163 | width: 488,
164 | url: '/src/static/hello.html',
165 | type: 'popup'
166 | })
167 | }
168 | localStorage.set({ hello: true })
169 | }
170 |
171 | // Add Listeners
172 | messenger.messageDisplay.onMessageDisplayed.addListener(onMessageDisplayed)
173 |
174 | // Init Data
175 | const [lowerBounds, upperBounds] = getBounds(storage)
176 | spamScores.setScoreBounds(lowerBounds, upperBounds)
177 |
178 | if (storage.customMailscannerHeaders) {
179 | spamScores.setCustomMailscannerHeaders(storage.customMailscannerHeaders)
180 | }
181 |
182 | // Set header order preferences
183 | if (storage.scoreHeaderOrder) {
184 | spamScores.setScoreHeaderOrder(storage.scoreHeaderOrder)
185 | }
186 | if (storage.scoreDetailsHeaderOrder) {
187 | spamScores.setScoreDetailsHeaderOrder(storage.scoreDetailsHeaderOrder)
188 | }
189 |
190 | spamScores.setHideIconScoreOptions(
191 | storage.hideIconScorePositive || false,
192 | storage.hideIconScoreNeutral || false,
193 | storage.hideIconScoreNegative || false
194 | )
195 | spamScores.addColumns('SpamScore', 'SpamScore (Icon)')
196 | }
197 | init()
198 |
--------------------------------------------------------------------------------
/src/browser/popup/popup.js:
--------------------------------------------------------------------------------
1 | import { SCORE_SYMBOLS } from './score_symbols.js'
2 | import {
3 | SCORE_DETAILS_ARRAY,
4 | SYMBOL_REGEX,
5 | HMAILSERVER_REASON_REGEX,
6 | DEFAULT_SCORE_DETAILS_ORDER
7 | } from '../../constants.js'
8 |
9 | messenger.tabs
10 | .query({
11 | active: true,
12 | currentWindow: true
13 | })
14 | .then(async tabs => {
15 | // Declaration / Values
16 | const tabId = tabs[0].id
17 | messenger.messageDisplay.getDisplayedMessage(tabId).then(async message => {
18 | const fullMessage = await messenger.messages.getFull(message.id)
19 | const allDetailScores = await getParsedDetailScores(fullMessage.headers)
20 | const parsedDetailScores = await deduplicateValues(allDetailScores)
21 | if (parsedDetailScores.length !== 0) {
22 | const groupedDetailScores = {
23 | positive: parsedDetailScores.filter(el => el.score > 0).sort((a, b) => b.score - a.score),
24 | negative: parsedDetailScores.filter(el => el.score < 0).sort((a, b) => a.score - b.score),
25 | neutral: parsedDetailScores.filter(el => el.score === 0).sort((a, b) => a.name.localeCompare(b.name))
26 | }
27 | let scoreDetailTable = document.querySelector('table#score-details')
28 | scoreDetailTable.style.display = 'block'
29 | const rowTemplate = document.querySelector('template#score-detail-row')
30 |
31 | for (const groupType of ['positive', 'negative', 'neutral']) {
32 | for (const detailElement of groupedDetailScores[groupType]) {
33 | // Get symbol description
34 | const symbolDescription = SCORE_SYMBOLS.find(sym => sym.name === detailElement.name)?.description
35 |
36 | // Clone template row
37 | let detailRow = document.importNode(rowTemplate.content, true)
38 |
39 | // Fill in data
40 | detailRow.querySelector('.score-detail-row').classList.add(groupType)
41 | detailRow.querySelector('.score').textContent = detailElement.score
42 | detailRow.querySelector('.name').textContent = detailElement.name || '-'
43 | if (symbolDescription) {
44 | detailRow.querySelector('.description').textContent = symbolDescription
45 | } else {
46 | detailRow.querySelector('.description').textContent = detailElement.description
47 | }
48 | if (detailElement.info) {
49 | detailRow.querySelector('.info').textContent = `[${detailElement.info}]`
50 | }
51 |
52 | // Add row to table
53 | scoreDetailTable.append(detailRow)
54 | }
55 | }
56 |
57 | // Workaround for a bug where Thunderbird does not correctly calculate the popup window height in Wide View layout (see issue #33)
58 | document.querySelector('body').style.maxHeight = `${window.screen.height / 2 - 60}px`
59 | } else {
60 | document.querySelector('#no-details').style.display = 'block'
61 | }
62 | })
63 | })
64 |
65 | /**
66 | * Parse the headers
67 | * @param {Object !scoreDetailsOrder.includes(h))]
80 |
81 | for (const headerName of headersToCheck) {
82 | if (headers[headerName]) {
83 | // Special handling for x-hmailserver-reason-score
84 | if (headerName === 'x-hmailserver-reason-score') {
85 | // Parse hMailServer reason headers
86 | for (const hdrName in headers) {
87 | if (HMAILSERVER_REASON_REGEX.test(hdrName)) {
88 | const headerValue = headers[hdrName][0]
89 | // Format: "Description - (Score: X)"
90 | const match = headerValue.match(/^(.+?)\s*-\s*\(Score:\s*([-+]?[0-9]+\.?[0-9]*)\)/)
91 | if (match) {
92 | const description = match[1].trim()
93 | const score = parseFloat(match[2])
94 | // Create a simplified name from the description
95 | const name = hdrName.toUpperCase().replace('X-HMAILSERVER-REASON-', 'REASON_')
96 | parsedDetailScores.push({
97 | name: name,
98 | score: score,
99 | info: '',
100 | description: description
101 | })
102 | }
103 | }
104 | }
105 | if (parsedDetailScores.length > 0) {
106 | break // Found details, stop looking
107 | }
108 | continue
109 | }
110 |
111 | // Regular header parsing
112 | let headerValue = headers[headerName][0] // For some reason thunderbird always saves it as an array
113 | if (headerName === 'x-spam-report' || headerName === 'x-ham-report') {
114 | const reportSplitted = headerValue.split('Content analysis details:')
115 | if (reportSplitted.length > 1) {
116 | headerValue = reportSplitted[1]
117 | }
118 | }
119 | headerValue = headerValue.trim().replace(/\r?\n/g, ' ')
120 | let symbolMatch = headerValue.match(SYMBOL_REGEX.prefix)
121 | if (symbolMatch && symbolMatch.length > 0) {
122 | const detailScore = symbolMatch.map(el => ({
123 | name: sanitizeRegexResult(el.replace(SYMBOL_REGEX.prefixSingle, '$2')),
124 | score: parseFloat(sanitizeRegexResult(el.replace(SYMBOL_REGEX.prefixSingle, '$1')) || 0),
125 | info: sanitizeRegexResult(el.replace(SYMBOL_REGEX.prefixSingle, '$4')) || '',
126 | description: sanitizeRegexResult(el.replace(SYMBOL_REGEX.prefixSingle, '$3')) || ''
127 | }))
128 | parsedDetailScores = [...parsedDetailScores, ...detailScore]
129 | // Use first matching header only (like for score headers)
130 | break
131 | }
132 |
133 | symbolMatch = headerValue.match(SYMBOL_REGEX.suffix)
134 | if (symbolMatch && symbolMatch.length > 0) {
135 | const detailScore = symbolMatch.map(el => ({
136 | name: sanitizeRegexResult(el.replace(SYMBOL_REGEX.suffix, '$1')),
137 | score: parseFloat(sanitizeRegexResult(el.replace(SYMBOL_REGEX.suffix, '$2')) || 0),
138 | info: sanitizeRegexResult(el.replace(SYMBOL_REGEX.suffix, '$3')) || ''
139 | }))
140 | parsedDetailScores = [...parsedDetailScores, ...detailScore]
141 | // Use first matching header only (like for score headers)
142 | break
143 | }
144 | }
145 | }
146 |
147 | return parsedDetailScores
148 | }
149 |
150 | /**
151 | * Removes duplicate spam scores from the parsed details
152 | * @param {array} scores
153 | * @returns {array}
154 | */
155 | async function deduplicateValues(scores) {
156 | // Some spam filters (like spamassassin) do add two distinct headers
157 | // with similar information to an email:
158 | // X-Spam-Status and X-Spam-Report where the first only contains a
159 | // list of checks that match an email whereas the latter contains
160 | // the corresponding scores and description as well. This leads to
161 | // double reporting of spam scores with different scores because
162 | // the parsing routine uses 0 to provide scores for tests where it
163 | // cannot find them.
164 | // See also https://github.com/friedPotat0/Spam-Scores/issues/48
165 | // To get rid of these duplicates, we use a two step approach:
166 | // 1. For all checks with a score of 0 the array is being examined
167 | // for another check of the same name that has a value being different
168 | // to 0 that can be used to update the score. (We do also add
169 | // descriptions provided by spamassassin in that step)
170 | // 2. Remove all duplicate checks from the array.
171 | // Unfortunately both passes do have a runtime of n^2.
172 |
173 | // 1. update all scores
174 | for (const el in scores) {
175 | for (const el2 in scores) {
176 | if (scores[el].name === scores[el2].name && scores[el2].score === 0) {
177 | if (scores[el].score > scores[el2].score || scores[el].score < scores[el2].score) {
178 | scores[el2].score = scores[el].score
179 | } else {
180 | scores[el].score = scores[el2].score
181 | }
182 | // while we're at it, update the description as well in
183 | // case it is missing
184 | if (!scores[el].description) {
185 | scores[el].description = scores[el2].description
186 | } else {
187 | scores[el2].description = scores[el].description
188 | }
189 | }
190 | }
191 | }
192 | // 2. remove duplicate checks -- https://stackoverflow.com/a/36744732
193 | const deduplicatedScores = scores.filter((el, index, self) => self.findIndex(el2 => el.name === el2.name) === index)
194 | return deduplicatedScores
195 | }
196 |
197 | /**
198 | * Trims and removes double whitespaces
199 | * @param {string} result
200 | * @returns {string}
201 | */
202 | function sanitizeRegexResult(result) {
203 | return result?.trim()?.replace(/\s\s+/g, ' ')
204 | }
205 |
--------------------------------------------------------------------------------
/src/typedef/general.js:
--------------------------------------------------------------------------------
1 | // For some reason it works for VSCode even if is not imported.
2 |
3 | // Constants
4 |
5 | /**
6 | * Defined Bounds for different scores
7 | * @typedef {object} interpolationBounds
8 | * @property {number} MIN_VALUE
9 | * @property {number} MAX_VALUE
10 | * @property {number} LOWER_BOUNDS
11 | * @property {number} UPPER_BOUNDS
12 | */
13 |
14 | // Program
15 |
16 | /**
17 | * Messenger namespace.
18 | * In Thunderbird, all WebExtension API can be accessed through the messenger.* namespace,
19 | * as with Firefox, but also through the messenger.* namespace, which is a better fit for Thunderbird.
20 | * https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API
21 | * https://webextension-api.thunderbird.net/en/91/#thunderbird-webextension-api-documentation
22 | * @namespace
23 | */
24 | const messenger = {
25 | storage: {
26 | /**
27 | * @type {StorageArea}
28 | */
29 | local: {},
30 | /**
31 | * @type {StorageArea}
32 | */
33 | sync: {},
34 | /**
35 | * @type {StorageArea}
36 | */
37 | managed: {}
38 | },
39 | messages: {
40 | /**
41 | * Returns a specified message, including all headers and MIME parts.
42 | *
43 | * Required permissions [messagesRead]
44 | * @param {number} messageId
45 | * @returns {MessagePart}
46 | */
47 | getFull: messageId => {}
48 | },
49 | /**
50 | * messagesRead is required to use messageDisplay.
51 | */
52 | messageDisplay: {
53 | /**
54 | * Fired when a message is displayed, whether in a 3-pane tab, a message tab, or a message window.
55 | *
56 | * Required permissions [messagesRead]
57 | */
58 | onMessageDisplayed: {
59 | /**
60 | * Called when message is displayed
61 | * @param {function(Tab, MessageHeader)} listener
62 | */
63 | addListener: listener => {}
64 | },
65 | /**
66 | * Gets the currently displayed message in the specified tab. It returns null if no messages are displayed, or if multiple messages are displayed.
67 | *
68 | * Required permissions [messagesRead]
69 | * @param {number} tabId
70 | * @returns {Promise}
71 | */
72 | getDisplayedMessage: tabId => {}
73 | },
74 | /**
75 | * A manifest entry named message_display_action is required to use messageDisplayAction.
76 | */
77 | messageDisplayAction: {
78 | /**
79 | * Disables the messageDisplayAction for a tab.
80 | * @param {number} [tabId] The id of the tab for which you want to modify the messageDisplayAction.
81 | */
82 | disable: tabId => {},
83 | /**
84 | * Enables the messageDisplayAction for a tab. By default, a messageDisplayAction is enabled.
85 | * @param {number} [tabId] The id of the tab for which you want to modify the messageDisplayAction.
86 | */
87 | enable: tabId => {},
88 | /**
89 | * Sets the title of the messageDisplayAction. This shows up in the tooltip and the label. Defaults to the add-on name.
90 | * @param {object} details
91 | * @param {string|null} details.title The string the messageDisplayAction should display as its label and when moused over.
92 | */
93 | setTitle: details => {},
94 | /**
95 | * Sets the icon for the messageDisplayAction.
96 | * The icon can be specified either as the path to an image file or as the pixel data from a canvas element,
97 | * or as dictionary of either one of those. Either the path or the imageData property must be specified.
98 | * @param {object} details
99 | * @param {ImageDataType | ImageDataDictionary} details.imageData (ImageDataType or ImageDataDictionary) - Either an ImageDataType object defining a single icon used for all sizes or an ImageDataDictionary object defining dedicated icons for different sizes.
100 | * @param {string | IconPathDictionary} details.path Either a relative image path defining a single icon used for all sizes or an IconPathDictionary object defining dedicated icons for different sizes.
101 | */
102 | setIcon: details => {}
103 | },
104 | tabs: {
105 | /**
106 | * Gets all tabs that have the specified properties, or all tabs if no properties are specified.
107 | * @param {object} [queryInfo]
108 | * @param {boolean} [queryInfo.active] Whether the tabs are active in their windows.
109 | * @param {boolean} [queryInfo.currentWindow] Whether the tabs are in the current window.
110 | * @param {boolean} [queryInfo.highlighted] Whether the tabs are highlighted. Works as an alias of active.
111 | * @param {number} [queryInfo.index] The position of the tabs within their windows.
112 | * @param {boolean} [queryInfo.lastFocusedWindow] Whether the tabs are in the last focused window.
113 | * @param {boolean} [queryInfo.mailTab] Whether the tab is a Thunderbird 3-pane tab.
114 | * @param {TabStatus} [queryInfo.status] Whether the tabs have completed loading.
115 | * @param {string} [queryInfo.title] Match page titles against a pattern.
116 | * @param {string} [queryInfo.type] – [Added in TB 91] Match tabs against the given Tab.type (see Tab). Ignored if queryInfo.mailTab is specified.
117 | * @param {string|string[]} [queryInfo.url] Match tabs against one or more URL Patterns. Note that fragment identifiers are not matched.
118 | * @param {number} [queryInfo.windowId] The ID of the parent window, or WINDOW_ID_CURRENT for the current window.
119 | * @param {WindowType} [queryInfo.windowType] The type of window the tabs are in.
120 | * @return {Promise} tabs
121 | */
122 | query: queryInfo => {}
123 | },
124 | i18n: {
125 | /**
126 | * Gets the localized string for the specified message.
127 | * @param {string} messageName
128 | * @param {string|string[]} [substitutions]
129 | * @returns {string} Message localized for current locale.
130 | */
131 | getMessage: (messageName, substitutions) => ''
132 | },
133 | /**
134 | * This module provides information about your extension and the environment it's running in.
135 | */
136 | runtime: {
137 | /**
138 | * Retrieves the Window object for the background page running inside the current extension.
139 | * @returns {Promise} A Promise that will be fulfilled with the Window object for the background page, if there is one.
140 | * If the extension does not include a background page, the promise is rejected with an error message.
141 | */
142 | getBackgroundPage: () => {}
143 | },
144 | /**
145 | * @type {SpamScores}
146 | */
147 | SpamScores: {}
148 | }
149 |
150 | /**
151 | * https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/storage/local
152 | * TODO: Incomplete
153 | * @typedef {object} StorageArea
154 | * @property {function(string[]=):Promise<(Object)>} get
155 | * @property {function(Object):Promise} set
156 | */
157 |
158 | /**
159 | * https://webextension-api.thunderbird.net/en/91/messages.html#messages-messagepart
160 | * @typedef {object} MessagePart
161 | * @property {string} body The content of the part
162 | * @property {string} contentType
163 | * @property {Object} IconPathDictionary
177 | */
178 |
179 | /**
180 | * Pixel data for an image. Must be an ImageData object (for example, from a canvas element).
181 | * @typedef {ImageData} ImageDataType
182 | */
183 |
184 | /**
185 | * A {size: ImageDataType} dictionary representing the icon to be set.
186 | * The actual ImageDataType to be used is chosen depending on the screen’s pixel density.
187 | * See the MDN documentation on browser styles for more information on this.
188 | * At least one ImageDataType must be specified.
189 | * https://webextension-api.thunderbird.net/en/91/messageDisplayAction.html#messagedisplayaction-imagedatadictionary
190 | * @typedef {Object.} ImageDataDictionary
191 | */
192 |
193 | /**
194 | * The type of a window. Under some circumstances a Window may not be assigned a type property.
195 | * @typedef {('normal'|'popup'|'panel'|'app'|'devtools'|'addressBook'|'messageCompose'|'messageDisplay')} WindowType
196 | */
197 |
198 | /**
199 | * Whether the tabs have completed loading.
200 | * @typedef {('loading'|'complete')} TabStatus
201 | */
202 |
203 | /**
204 | * https://webextension-api.thunderbird.net/en/91/tabs.html#tabs-tab
205 | * TODO: Incomplete
206 | * @typedef {object} Tab
207 | * @property {number} id The ID of the tab. Tab IDs are unique within a browser session.
208 | * Under some circumstances a Tab may not be assigned an ID.
209 | * Tab ID can also be set to TAB_ID_NONE for apps and devtools windows.
210 | */
211 |
212 | /**
213 | * https://webextension-api.thunderbird.net/en/91/messages.html#messages-messageheader
214 | * TODO: Incomplete
215 | * @typedef {object} MessageHeader
216 | * @property {number} id
217 | */
218 |
219 | /**
220 | * Custom
221 | * @typedef {object} parsedDetailScores
222 | * @property {string} name
223 | * @property {number} score
224 | * @property {string} info
225 | * @property {string} [description]
226 | */
227 |
228 | /**
229 | * An object describing a mail folder.
230 | * https://webextension-api.thunderbird.net/en/91/folders.html#folders-mailfolder
231 | * TODO: Incomplete
232 | * @typedef {object} MailFolder
233 | * @property {string} path Path to this folder in the account. Although paths look predictable, never guess a folder’s path, as there are a number of reasons why it may not be what you think it is.
234 | */
235 |
--------------------------------------------------------------------------------
/src/options/options.js:
--------------------------------------------------------------------------------
1 | import {
2 | DEFAULT_SCORE_LOWER_BOUNDS,
3 | DEFAULT_SCORE_UPPER_BOUNDS,
4 | DEFAULT_SCORE_HEADER_ORDER,
5 | DEFAULT_SCORE_DETAILS_ORDER
6 | } from '../constants.js'
7 | import { getBounds } from '../functions.js'
8 |
9 | // Variables
10 | const localStorage = messenger.storage.local
11 | const i18n = messenger.i18n
12 |
13 | // DOM variables
14 | const inputScoreBoundsLower = document.getElementById('score-bounds-lower')
15 | const inputScoreBoundsUpper = document.getElementById('score-bounds-upper')
16 | const inputScoreBoundsBetween = document.getElementById('score-bounds-between')
17 | const checkboxIconScorePositive = document.getElementById('hide-icon-score-positive')
18 | const checkboxIconScoreNeutral = document.getElementById('hide-icon-score-neutral')
19 | const checkboxIconScoreNegative = document.getElementById('hide-icon-score-negative')
20 | const scoreHeadersList = document.getElementById('score-headers-list')
21 | const scoreDetailsHeadersList = document.getElementById('score-details-headers-list')
22 | const resetScoreHeadersBtn = document.getElementById('reset-score-headers')
23 | const resetScoreDetailsHeadersBtn = document.getElementById('reset-score-details-headers')
24 |
25 | // Translations
26 | for (const i18nKey of [
27 | 'optionsIconRanges',
28 | 'optionsScoreGreater',
29 | 'optionsScoreLess',
30 | 'optionsHeaderPriority',
31 | 'optionsHeaderPriorityDescription',
32 | 'optionsScoreHeaders',
33 | 'optionsScoreDetailsHeaders'
34 | ]) {
35 | const element = document.querySelector('*[data-i18n="' + i18nKey + '"]')
36 | if (element) element.textContent = i18n.getMessage(i18nKey)
37 | }
38 | document
39 | .querySelectorAll('*[data-i18n="optionsHideIconAndScore"]')
40 | .forEach(el => (el.textContent = i18n.getMessage('optionsHideIconAndScore')))
41 | document
42 | .querySelectorAll('*[data-i18n="optionsResetToDefault"]')
43 | .forEach(el => (el.textContent = i18n.getMessage('optionsResetToDefault')))
44 |
45 | // DOM events
46 | inputScoreBoundsLower.addEventListener('change', saveScoreLower)
47 | inputScoreBoundsUpper.addEventListener('change', saveScoreUpper)
48 | checkboxIconScorePositive.addEventListener('change', saveIconPositive)
49 | checkboxIconScoreNeutral.addEventListener('change', saveIconNeutral)
50 | checkboxIconScoreNegative.addEventListener('change', saveIconNegative)
51 | resetScoreHeadersBtn.addEventListener('click', resetScoreHeadersOrder)
52 | resetScoreDetailsHeadersBtn.addEventListener('click', resetScoreDetailsHeadersOrder)
53 |
54 | async function init() {
55 | // Load values from storage
56 | const storage = await localStorage.get([
57 | 'scoreIconLowerBounds',
58 | 'scoreIconUpperBounds',
59 | 'hideIconScorePositive',
60 | 'hideIconScoreNeutral',
61 | 'hideIconScoreNegative',
62 | 'scoreHeaderOrder',
63 | 'scoreDetailsHeaderOrder'
64 | ])
65 |
66 | const [lowerBounds, upperBounds] = getBounds(storage)
67 |
68 | // Set values
69 | inputScoreBoundsLower.value = lowerBounds
70 | inputScoreBoundsUpper.value = upperBounds
71 | inputScoreBoundsBetween.textContent = i18n.getMessage('optionsScoreBetween', [lowerBounds, upperBounds])
72 | messenger.SpamScores.setScoreBounds(lowerBounds, upperBounds)
73 |
74 | checkboxIconScorePositive.checked = storage.hideIconScorePositive || false
75 | checkboxIconScoreNeutral.checked = storage.hideIconScoreNeutral || false
76 | checkboxIconScoreNegative.checked = storage.hideIconScoreNegative || false
77 |
78 | // Initialize header order lists
79 | const scoreHeaderOrder = storage.scoreHeaderOrder || DEFAULT_SCORE_HEADER_ORDER
80 | const scoreDetailsHeaderOrder = storage.scoreDetailsHeaderOrder || DEFAULT_SCORE_DETAILS_ORDER
81 |
82 | renderSortableList(scoreHeadersList, scoreHeaderOrder, 'scoreHeaderOrder')
83 | renderSortableList(scoreDetailsHeadersList, scoreDetailsHeaderOrder, 'scoreDetailsHeaderOrder')
84 | }
85 | init()
86 |
87 | function saveIconPositive() {
88 | const hideIconScorePositive = checkboxIconScorePositive.checked
89 | localStorage.set({ hideIconScorePositive })
90 | saveIcons()
91 | }
92 |
93 | function saveIconNeutral() {
94 | const hideIconScoreNeutral = checkboxIconScoreNeutral.checked
95 | localStorage.set({ hideIconScoreNeutral })
96 | saveIcons()
97 | }
98 |
99 | function saveIconNegative() {
100 | const hideIconScoreNegative = checkboxIconScoreNegative.checked
101 | localStorage.set({ hideIconScoreNegative })
102 | saveIcons()
103 | }
104 |
105 | function saveIcons() {
106 | // Get rest of values
107 | const hideIconScorePositive = checkboxIconScorePositive.checked
108 | const hideIconScoreNeutral = checkboxIconScoreNeutral.checked
109 | const hideIconScoreNegative = checkboxIconScoreNegative.checked
110 |
111 | messenger.SpamScores.setHideIconScoreOptions(hideIconScorePositive, hideIconScoreNeutral, hideIconScoreNegative)
112 | }
113 |
114 | async function saveScoreLower() {
115 | const scoreBoundsLower = inputScoreBoundsLower.value
116 | const storage = await localStorage.get(['scoreIconLowerBounds', 'scoreIconUpperBounds'])
117 | const newUpperBounds = parseFloat(storage.scoreIconUpperBounds || DEFAULT_SCORE_UPPER_BOUNDS)
118 |
119 | if (scoreBoundsLower !== '') {
120 | let newLowerBounds = parseFloat(scoreBoundsLower)
121 | try {
122 | if (newLowerBounds > newUpperBounds) throw Error('Upper score cannot be lower than lower bounds')
123 | if (newLowerBounds < -10000) throw Error('Wrong score lower bounds')
124 | localStorage.set({ scoreIconLowerBounds: newLowerBounds })
125 | saveScores(newLowerBounds, newUpperBounds)
126 | } catch (error) {
127 | // Restore previously saved bounds or fallback to defaults
128 | newLowerBounds = parseFloat(storage.scoreIconLowerBounds || DEFAULT_SCORE_LOWER_BOUNDS)
129 | }
130 | inputScoreBoundsLower.value = newLowerBounds // number
131 | }
132 | }
133 |
134 | async function saveScoreUpper() {
135 | const scoreBoundsUpper = inputScoreBoundsUpper.value
136 | const storage = await localStorage.get(['scoreIconLowerBounds', 'scoreIconUpperBounds'])
137 | const newLowerBounds = parseFloat(storage.scoreIconLowerBounds || DEFAULT_SCORE_LOWER_BOUNDS)
138 |
139 | if (scoreBoundsUpper !== '') {
140 | let newUpperBounds = parseFloat(scoreBoundsUpper)
141 | try {
142 | if (newLowerBounds > newUpperBounds) throw Error('Upper score cannot be lower than lower bounds')
143 | if (newUpperBounds > 10000) throw Error('Wrong score upper bounds')
144 | localStorage.set({ scoreIconUpperBounds: newUpperBounds })
145 | saveScores(newLowerBounds, newUpperBounds)
146 | } catch (error) {
147 | // Restore previously saved bounds or fallback to defaults
148 | newUpperBounds = parseFloat(storage.scoreIconUpperBounds || DEFAULT_SCORE_UPPER_BOUNDS)
149 | }
150 | inputScoreBoundsUpper.value = newUpperBounds // number
151 | }
152 | }
153 |
154 | function saveScores(lower, upper) {
155 | inputScoreBoundsBetween.textContent = i18n.getMessage('optionsScoreBetween', [lower, upper])
156 | messenger.SpamScores.setScoreBounds(lower, upper)
157 | }
158 |
159 | /**
160 | * Render a sortable list of headers
161 | * @param {HTMLElement} container - The container element for the list
162 | * @param {string[]} headers - Array of header names
163 | * @param {string} storageKey - Key for localStorage
164 | */
165 | function renderSortableList(container, headers, storageKey) {
166 | container.innerHTML = ''
167 |
168 | headers.forEach((header, index) => {
169 | const item = document.createElement('div')
170 | item.className = 'sortable-item'
171 | item.textContent = header
172 | item.draggable = true
173 | item.dataset.index = index
174 |
175 | item.addEventListener('dragstart', handleDragStart)
176 | item.addEventListener('dragover', handleDragOver)
177 | item.addEventListener('drop', e => handleDrop(e, container, storageKey))
178 | item.addEventListener('dragend', handleDragEnd)
179 |
180 | container.appendChild(item)
181 | })
182 | }
183 |
184 | let draggedElement = null
185 |
186 | function handleDragStart(e) {
187 | draggedElement = e.target
188 | e.target.classList.add('dragging')
189 | e.dataTransfer.effectAllowed = 'move'
190 | }
191 |
192 | function handleDragOver(e) {
193 | if (e.preventDefault) {
194 | e.preventDefault()
195 | }
196 | e.dataTransfer.dropEffect = 'move'
197 | return false
198 | }
199 |
200 | function handleDrop(e, container, storageKey) {
201 | if (e.stopPropagation) {
202 | e.stopPropagation()
203 | }
204 |
205 | if (draggedElement !== e.target && e.target.classList.contains('sortable-item')) {
206 | // Get all items
207 | const items = Array.from(container.querySelectorAll('.sortable-item'))
208 | const draggedIndex = items.indexOf(draggedElement)
209 | const targetIndex = items.indexOf(e.target)
210 |
211 | // Reorder in DOM
212 | if (draggedIndex < targetIndex) {
213 | e.target.parentNode.insertBefore(draggedElement, e.target.nextSibling)
214 | } else {
215 | e.target.parentNode.insertBefore(draggedElement, e.target)
216 | }
217 |
218 | // Save new order
219 | saveHeaderOrder(container, storageKey)
220 | }
221 |
222 | return false
223 | }
224 |
225 | function handleDragEnd(e) {
226 | e.target.classList.remove('dragging')
227 | }
228 |
229 | /**
230 | * Save the current order of headers to localStorage
231 | * @param {HTMLElement} container - The container element
232 | * @param {string} storageKey - Key for localStorage
233 | */
234 | async function saveHeaderOrder(container, storageKey) {
235 | const items = Array.from(container.querySelectorAll('.sortable-item'))
236 | const order = items.map(item => item.textContent)
237 |
238 | await localStorage.set({ [storageKey]: order })
239 |
240 | // Update experiments.js
241 | if (storageKey === 'scoreHeaderOrder') {
242 | messenger.SpamScores.setScoreHeaderOrder(order)
243 | } else if (storageKey === 'scoreDetailsHeaderOrder') {
244 | messenger.SpamScores.setScoreDetailsHeaderOrder(order)
245 | }
246 | }
247 |
248 | /**
249 | * Reset score headers to default order
250 | */
251 | async function resetScoreHeadersOrder() {
252 | await localStorage.set({ scoreHeaderOrder: DEFAULT_SCORE_HEADER_ORDER })
253 | renderSortableList(scoreHeadersList, DEFAULT_SCORE_HEADER_ORDER, 'scoreHeaderOrder')
254 | messenger.SpamScores.setScoreHeaderOrder(DEFAULT_SCORE_HEADER_ORDER)
255 | }
256 |
257 | /**
258 | * Reset score details headers to default order
259 | */
260 | async function resetScoreDetailsHeadersOrder() {
261 | await localStorage.set({ scoreDetailsHeaderOrder: DEFAULT_SCORE_DETAILS_ORDER })
262 | renderSortableList(scoreDetailsHeadersList, DEFAULT_SCORE_DETAILS_ORDER, 'scoreDetailsHeaderOrder')
263 | messenger.SpamScores.setScoreDetailsHeaderOrder(DEFAULT_SCORE_DETAILS_ORDER)
264 | }
265 |
--------------------------------------------------------------------------------
/src/experiments/experiments.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | // Copy of constants.js until Experiments API supports ES6 modules
4 | const CUSTOM_SCORE_REGEX = {
5 | 'mailscanner-spamcheck':
6 | /(?:score|punteggio|puntuació|sgor\/score|skore|Wertung|bedømmelse|puntaje|pont|escore|resultat|skore)=([-+]?[0-9]+\.?[0-9]*),/
7 | }
8 |
9 | // Copy of constants.js until Experiments API supports ES6 modules
10 | const SCORE_REGEX = {
11 | 'x-spamd-result': /\[([-+]?[0-9]+\.?[0-9]*) \/ [-+]?[0-9]+\.?[0-9]*\];/,
12 | 'x-spam-status': /(?:Yes|No)(?:, score=|\/)([-+]?[0-9]+\.?[0-9]*)/,
13 | 'x-rspam-status': /(?:Yes|No)(?:, score=|\/)([-+]?[0-9]+\.?[0-9]*)/,
14 | 'x-spam-score': /([-+]?[0-9]+\.?[0-9]*)/,
15 | 'x-spam-report': /([-+]?[0-9]+\.?[0-9]*) hits,/,
16 | 'x-ham-report': /([-+]?[0-9]+\.?[0-9]*) hits,/,
17 | 'x-rspamd-score': /([-+]?[0-9]+\.?[0-9]*)/,
18 | 'x-vr-spamscore': /([0-9]+)/,
19 | 'x-hmailserver-reason-score': /([-+]?[0-9]+\.?[0-9]*)/
20 | }
21 |
22 | // Default order for parsing score headers
23 | const DEFAULT_SCORE_HEADER_ORDER = [
24 | 'x-spamd-result',
25 | 'x-spam-status',
26 | 'x-rspam-status',
27 | 'x-spam-score',
28 | 'x-spam-report',
29 | 'x-ham-report',
30 | 'x-rspamd-score',
31 | 'x-vr-spamscore',
32 | 'x-hmailserver-reason-score'
33 | ]
34 |
35 | function importThreadPaneColumnsModule() {
36 | try {
37 | // TB115
38 | return ChromeUtils.importESModule('chrome://messenger/content/thread-pane-columns.mjs')
39 | } catch (err) {
40 | // TB128
41 | return ChromeUtils.importESModule('chrome://messenger/content/ThreadPaneColumns.mjs')
42 | }
43 | }
44 |
45 | var { ThreadPaneColumns } = importThreadPaneColumnsModule()
46 |
47 | const DEFAULT_SCORE_LOWER_BOUNDS = -2
48 | const DEFAULT_SCORE_UPPER_BOUNDS = 2
49 |
50 | let scoreHdrViewParams = {
51 | lowerScoreBounds: DEFAULT_SCORE_LOWER_BOUNDS,
52 | upperScoreBounds: DEFAULT_SCORE_UPPER_BOUNDS,
53 | scoreHeaderOrder: DEFAULT_SCORE_HEADER_ORDER
54 | }
55 |
56 | function getScore(hdr) {
57 | // Use custom order if available, otherwise use default
58 | const headerOrder = scoreHdrViewParams.scoreHeaderOrder || DEFAULT_SCORE_HEADER_ORDER
59 |
60 | for (const regExName of headerOrder) {
61 | if (!SCORE_REGEX[regExName]) continue // Skip if regex not defined
62 | const headerValue = hdr.getStringProperty(regExName)
63 | if (headerValue === '') continue
64 | const scoreField = headerValue.match(SCORE_REGEX[regExName])
65 | if (!scoreField) continue // If no match iterate - Note: This shouldn't be needed
66 | const score = parseFloat(scoreField[1])
67 | if (!isNaN(score)) return score
68 | }
69 |
70 | if (scoreHdrViewParams.customMailscannerHeaders) {
71 | for (const headerName of scoreHdrViewParams.customMailscannerHeaders) {
72 | for (const regExName in CUSTOM_SCORE_REGEX) {
73 | if (headerName.endsWith(regExName)) {
74 | const headerValue = hdr.getStringProperty(headerName)
75 | const scoreField = headerValue.match(CUSTOM_SCORE_REGEX[regExName])
76 | if (!scoreField) continue // If no match iterate
77 | const score = parseFloat(scoreField[1])
78 | if (!isNaN(score)) return score
79 | }
80 | }
81 | }
82 | }
83 | return null
84 | }
85 |
86 | function getSortScore(hdr) {
87 | const score = getScore(hdr)
88 | if (score === null) return null
89 | // Multiply by 100000 for decimal precision, then add offset of 1 billion to handle negative numbers
90 | // This ensures both negative and positive scores sort correctly
91 | return Math.round(score * 100000) + 1000000000
92 | }
93 |
94 | /**
95 | * For debugging Experiments.js, press CTRL + SHIFT + U/I
96 | */
97 |
98 | /**
99 | * Do not change var because it's a global class
100 | * https://webextension-api.thunderbird.net/en/91/how-to/experiments.html#implementing-functions
101 | */
102 | var SpamScores = class extends ExtensionAPI {
103 | /**
104 | * Called on startup and on reload
105 | */
106 | onStartup() {
107 | updatePrefs()
108 | }
109 |
110 | /**
111 | * Called when the extension is disabled, removed, reloaded, or Thunderbird closes.
112 | * @param {boolean} isAppShutdown
113 | */
114 | onShutdown(isAppShutdown) {
115 | if (isAppShutdown) return
116 | /**
117 | * This method is called to notify all observers for a particular topic. See Example.
118 | * .notifyObservers(null, "myTopicID", "someAdditionalInformationPassedAs'Data'Parameter");
119 | *
120 | * void notifyObservers(in nsISupports aSubject, in string aTopic, in wstring someData);
121 | * aSubject A notification specific interface pointer. This usually corresponds to the source of the notification, but could be defined differently depending on the notification topic and may even be null.
122 | * aTopic The notification topic. This string-valued key uniquely identifies the notification. This parameter must not be null.
123 | * someData A notification specific string value. The meaning of this parameter is dependent on the topic. It may be null.
124 | */
125 | Services.obs.notifyObservers(null, 'startupcache-invalidate')
126 | }
127 |
128 | /**
129 | *
130 | * @param {*} context
131 | * @returns
132 | */
133 | getAPI(context) {
134 | context.callOnClose(this)
135 | // All functions should be added in schema.json
136 | return {
137 | SpamScores: {
138 | setScoreBounds(lower, upper) {
139 | scoreHdrViewParams.lowerScoreBounds = lower
140 | scoreHdrViewParams.upperScoreBounds = upper
141 | },
142 | setHideIconScoreOptions(hidePositive, hideNeutral, hideNegative) {
143 | scoreHdrViewParams.hideIconScorePositive = hidePositive
144 | scoreHdrViewParams.hideIconScoreNeutral = hideNeutral
145 | scoreHdrViewParams.hideIconScoreNegative = hideNegative
146 | },
147 | setCustomMailscannerHeaders(customMailscannerHeaders) {
148 | scoreHdrViewParams.customMailscannerHeaders = customMailscannerHeaders
149 | },
150 | setScoreHeaderOrder(scoreHeaderOrder) {
151 | scoreHdrViewParams.scoreHeaderOrder = scoreHeaderOrder
152 | },
153 | setScoreDetailsHeaderOrder(scoreDetailsHeaderOrder) {
154 | scoreHdrViewParams.scoreDetailsHeaderOrder = scoreDetailsHeaderOrder
155 | },
156 | addHeadersToPrefs(dynamicHeaders) {
157 | updatePrefs(dynamicHeaders)
158 | },
159 | // Deprecated (Fallback for add-on version <= 1.3.1)
160 | getHelloFlag() {
161 | try {
162 | return Services.prefs.getBoolPref('spamscores.hello')
163 | } catch (err) {
164 | return false
165 | }
166 | },
167 |
168 | async addColumns(nameSpamScoreValue, nameSpamScoreIcon) {
169 | function getExtensionUrl(url) {
170 | if (url) {
171 | return context.extension.baseURI.resolve(url)
172 | }
173 | return null
174 | }
175 | function scoreCallback(msgHdr) {
176 | let score = getScore(msgHdr)
177 | if (score === null) return null
178 | if (score > scoreHdrViewParams.upperScoreBounds && scoreHdrViewParams.hideIconScorePositive) return null
179 | if (
180 | score <= scoreHdrViewParams.upperScoreBounds &&
181 | score >= scoreHdrViewParams.lowerScoreBounds &&
182 | scoreHdrViewParams.hideIconScoreNeutral
183 | )
184 | return null
185 | if (score < scoreHdrViewParams.lowerScoreBounds && scoreHdrViewParams.hideIconScoreNegative) return null
186 | return score
187 | }
188 |
189 | ThreadPaneColumns.addCustomColumn('spam-score-value', {
190 | name: nameSpamScoreValue,
191 | hidden: true,
192 | icon: false,
193 | resizable: true,
194 | sortable: true,
195 | sortCallback: getSortScore,
196 | textCallback: scoreCallback
197 | })
198 |
199 | ThreadPaneColumns.addCustomColumn('spam-score-icon', {
200 | name: nameSpamScoreIcon,
201 | hidden: true,
202 | icon: true,
203 | iconHeaderUrl: getExtensionUrl('/images/icon-16px.png'),
204 | iconCellDefinitions: [
205 | {
206 | id: 'positive',
207 | alt: '+',
208 | title: 'Positive Spam Score',
209 | url: getExtensionUrl('/images/score_positive.png')
210 | },
211 | {
212 | id: 'negative',
213 | alt: '-',
214 | title: 'Negative Spam Score',
215 | url: getExtensionUrl('/images/score_negative.png')
216 | },
217 | {
218 | id: 'neutral',
219 | alt: '0',
220 | title: 'Neutral Spam Score',
221 | url: getExtensionUrl('/images/score_neutral.png')
222 | }
223 | ],
224 | iconCallback: msgHdr => {
225 | let score = getScore(msgHdr)
226 | if (score === null) return ''
227 | if (!scoreHdrViewParams.hideIconScorePositive && score > scoreHdrViewParams.upperScoreBounds)
228 | return 'positive'
229 | if (
230 | !scoreHdrViewParams.hideIconScoreNeutral &&
231 | score <= scoreHdrViewParams.upperScoreBounds &&
232 | score >= scoreHdrViewParams.lowerScoreBounds
233 | )
234 | return 'neutral'
235 | if (!scoreHdrViewParams.hideIconScoreNegative && score < scoreHdrViewParams.lowerScoreBounds)
236 | return 'negative'
237 | return ''
238 | },
239 | resizable: false,
240 | sortable: true,
241 | sortCallback: getSortScore,
242 | textCallback: scoreCallback
243 | })
244 | },
245 |
246 | async removeColumns(id) {
247 | ThreadPaneColumns.removeCustomColumn('spam-score-value')
248 | ThreadPaneColumns.removeCustomColumn('spam-score-icon')
249 | }
250 | }
251 | }
252 | }
253 |
254 | close() {
255 | ThreadPaneColumns.removeCustomColumn('spam-score-value')
256 | ThreadPaneColumns.removeCustomColumn('spam-score-icon')
257 | }
258 | }
259 |
260 | /**
261 | * This is what it lets nsIMsgDBHdr have the properties of the headers
262 | * http://kb.mozillazine.org/Mail_and_news_settings // 2019
263 | * http://udn.realityripple.com/docs/Mozilla/Tech/XPCOM/Reference/Interface/nsIPrefBranch
264 | * https://searchfox.org/comm-central/source/mailnews/mailnews.js // 2021
265 | * Requirements: Repair Folders then Restart
266 | * @param {string[]} dynamicHeaders
267 | */
268 | function updatePrefs(dynamicHeaders = []) {
269 | const mailnews = Services.prefs.getBranch('mailnews')
270 | // Copy of constants.js until support of ES6 modules
271 | const staticHeaders = [
272 | 'x-spam-score',
273 | 'x-rspamd-score',
274 | 'x-vr-spamscore',
275 | 'x-spamd-result',
276 | 'x-spam-status',
277 | 'x-rspam-status',
278 | 'x-spam-report',
279 | 'x-ham-report',
280 | 'x-hmailserver-reason-score'
281 | ]
282 | const headers = [...staticHeaders, ...dynamicHeaders]
283 |
284 | // customDBHeaders: String in the form of "header1 header2 header3"
285 | // Note: Do not overwrite headers of other add-ons or user-defined ones. Always append new headers!
286 | const existingCustomDBHeaders = Services.prefs.getCharPref('mailnews.customDBHeaders').trim()
287 | // Split existing headers and filter out new ones that already exist
288 | const existingDBHeadersArray = existingCustomDBHeaders ? existingCustomDBHeaders.split(/\s+/) : []
289 | let newCustomDBHeaders = headers.filter(el => !existingDBHeadersArray.includes(el))
290 | if (newCustomDBHeaders.length > 0) {
291 | const separator = existingCustomDBHeaders ? ' ' : ''
292 | newCustomDBHeaders = `${existingCustomDBHeaders}${separator}${newCustomDBHeaders.join(' ')}`
293 | mailnews.setCharPref('.customDBHeaders', newCustomDBHeaders)
294 | }
295 |
296 | // customHeaders: String in the form of "header1: header2: header3:"
297 | // Note: Do not overwrite headers of other add-ons or user-defined ones. Always append new headers!
298 | const existingCustomHeaders = Services.prefs.getCharPref('mailnews.customHeaders').trim()
299 | // Split existing headers by ": " and filter out new ones that already exist
300 | const existingHeadersArray = existingCustomHeaders
301 | ? existingCustomHeaders
302 | .split(/:\s*/)
303 | .filter(h => h)
304 | .map(h => h.trim())
305 | : []
306 | let newCustomHeaders = headers.filter(el => !existingHeadersArray.includes(el))
307 | if (newCustomHeaders.length > 0) {
308 | // Ensure proper format: existing headers should end with ":" and new headers should be separated by ": "
309 | // If existing headers exist and don't end with ":", add ": " before new headers
310 | // If existing headers end with ":", add " " before new headers
311 | let prefix = existingCustomHeaders
312 | if (existingCustomHeaders) {
313 | if (existingCustomHeaders.endsWith(':')) {
314 | prefix = existingCustomHeaders + ' '
315 | } else if (existingCustomHeaders.endsWith(': ')) {
316 | prefix = existingCustomHeaders
317 | } else {
318 | prefix = existingCustomHeaders + ': '
319 | }
320 | }
321 | newCustomHeaders = `${prefix}${newCustomHeaders.join(': ')}:`
322 | mailnews.setCharPref('.customHeaders', newCustomHeaders)
323 | }
324 |
325 | /**
326 | * PREF_INVALID 0 long
327 | * PREF_STRING 32 long data type.
328 | * PREF_INT 64 long data type.
329 | * PREF_BOOL 128 long data type.
330 | */
331 | // console.log(mailnews.getPrefType('.customDBHeaders'))
332 | }
333 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Attribution-NonCommercial-ShareAlike 4.0 International
2 |
3 | =======================================================================
4 |
5 | Creative Commons Corporation ("Creative Commons") is not a law firm and
6 | does not provide legal services or legal advice. Distribution of
7 | Creative Commons public licenses does not create a lawyer-client or
8 | other relationship. Creative Commons makes its licenses and related
9 | information available on an "as-is" basis. Creative Commons gives no
10 | warranties regarding its licenses, any material licensed under their
11 | terms and conditions, or any related information. Creative Commons
12 | disclaims all liability for damages resulting from their use to the
13 | fullest extent possible.
14 |
15 | Using Creative Commons Public Licenses
16 |
17 | Creative Commons public licenses provide a standard set of terms and
18 | conditions that creators and other rights holders may use to share
19 | original works of authorship and other material subject to copyright
20 | and certain other rights specified in the public license below. The
21 | following considerations are for informational purposes only, are not
22 | exhaustive, and do not form part of our licenses.
23 |
24 | Considerations for licensors: Our public licenses are
25 | intended for use by those authorized to give the public
26 | permission to use material in ways otherwise restricted by
27 | copyright and certain other rights. Our licenses are
28 | irrevocable. Licensors should read and understand the terms
29 | and conditions of the license they choose before applying it.
30 | Licensors should also secure all rights necessary before
31 | applying our licenses so that the public can reuse the
32 | material as expected. Licensors should clearly mark any
33 | material not subject to the license. This includes other CC-
34 | licensed material, or material used under an exception or
35 | limitation to copyright. More considerations for licensors:
36 | wiki.creativecommons.org/Considerations_for_licensors
37 |
38 | Considerations for the public: By using one of our public
39 | licenses, a licensor grants the public permission to use the
40 | licensed material under specified terms and conditions. If
41 | the licensor's permission is not necessary for any reason--for
42 | example, because of any applicable exception or limitation to
43 | copyright--then that use is not regulated by the license. Our
44 | licenses grant only permissions under copyright and certain
45 | other rights that a licensor has authority to grant. Use of
46 | the licensed material may still be restricted for other
47 | reasons, including because others have copyright or other
48 | rights in the material. A licensor may make special requests,
49 | such as asking that all changes be marked or described.
50 | Although not required by our licenses, you are encouraged to
51 | respect those requests where reasonable. More considerations
52 | for the public:
53 | wiki.creativecommons.org/Considerations_for_licensees
54 |
55 | =======================================================================
56 |
57 | Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International
58 | Public License
59 |
60 | By exercising the Licensed Rights (defined below), You accept and agree
61 | to be bound by the terms and conditions of this Creative Commons
62 | Attribution-NonCommercial-ShareAlike 4.0 International Public License
63 | ("Public License"). To the extent this Public License may be
64 | interpreted as a contract, You are granted the Licensed Rights in
65 | consideration of Your acceptance of these terms and conditions, and the
66 | Licensor grants You such rights in consideration of benefits the
67 | Licensor receives from making the Licensed Material available under
68 | these terms and conditions.
69 |
70 |
71 | Section 1 -- Definitions.
72 |
73 | a. Adapted Material means material subject to Copyright and Similar
74 | Rights that is derived from or based upon the Licensed Material
75 | and in which the Licensed Material is translated, altered,
76 | arranged, transformed, or otherwise modified in a manner requiring
77 | permission under the Copyright and Similar Rights held by the
78 | Licensor. For purposes of this Public License, where the Licensed
79 | Material is a musical work, performance, or sound recording,
80 | Adapted Material is always produced where the Licensed Material is
81 | synched in timed relation with a moving image.
82 |
83 | b. Adapter's License means the license You apply to Your Copyright
84 | and Similar Rights in Your contributions to Adapted Material in
85 | accordance with the terms and conditions of this Public License.
86 |
87 | c. BY-NC-SA Compatible License means a license listed at
88 | creativecommons.org/compatiblelicenses, approved by Creative
89 | Commons as essentially the equivalent of this Public License.
90 |
91 | d. Copyright and Similar Rights means copyright and/or similar rights
92 | closely related to copyright including, without limitation,
93 | performance, broadcast, sound recording, and Sui Generis Database
94 | Rights, without regard to how the rights are labeled or
95 | categorized. For purposes of this Public License, the rights
96 | specified in Section 2(b)(1)-(2) are not Copyright and Similar
97 | Rights.
98 |
99 | e. Effective Technological Measures means those measures that, in the
100 | absence of proper authority, may not be circumvented under laws
101 | fulfilling obligations under Article 11 of the WIPO Copyright
102 | Treaty adopted on December 20, 1996, and/or similar international
103 | agreements.
104 |
105 | f. Exceptions and Limitations means fair use, fair dealing, and/or
106 | any other exception or limitation to Copyright and Similar Rights
107 | that applies to Your use of the Licensed Material.
108 |
109 | g. License Elements means the license attributes listed in the name
110 | of a Creative Commons Public License. The License Elements of this
111 | Public License are Attribution, NonCommercial, and ShareAlike.
112 |
113 | h. Licensed Material means the artistic or literary work, database,
114 | or other material to which the Licensor applied this Public
115 | License.
116 |
117 | i. Licensed Rights means the rights granted to You subject to the
118 | terms and conditions of this Public License, which are limited to
119 | all Copyright and Similar Rights that apply to Your use of the
120 | Licensed Material and that the Licensor has authority to license.
121 |
122 | j. Licensor means the individual(s) or entity(ies) granting rights
123 | under this Public License.
124 |
125 | k. NonCommercial means not primarily intended for or directed towards
126 | commercial advantage or monetary compensation. For purposes of
127 | this Public License, the exchange of the Licensed Material for
128 | other material subject to Copyright and Similar Rights by digital
129 | file-sharing or similar means is NonCommercial provided there is
130 | no payment of monetary compensation in connection with the
131 | exchange.
132 |
133 | l. Share means to provide material to the public by any means or
134 | process that requires permission under the Licensed Rights, such
135 | as reproduction, public display, public performance, distribution,
136 | dissemination, communication, or importation, and to make material
137 | available to the public including in ways that members of the
138 | public may access the material from a place and at a time
139 | individually chosen by them.
140 |
141 | m. Sui Generis Database Rights means rights other than copyright
142 | resulting from Directive 96/9/EC of the European Parliament and of
143 | the Council of 11 March 1996 on the legal protection of databases,
144 | as amended and/or succeeded, as well as other essentially
145 | equivalent rights anywhere in the world.
146 |
147 | n. You means the individual or entity exercising the Licensed Rights
148 | under this Public License. Your has a corresponding meaning.
149 |
150 |
151 | Section 2 -- Scope.
152 |
153 | a. License grant.
154 |
155 | 1. Subject to the terms and conditions of this Public License,
156 | the Licensor hereby grants You a worldwide, royalty-free,
157 | non-sublicensable, non-exclusive, irrevocable license to
158 | exercise the Licensed Rights in the Licensed Material to:
159 |
160 | a. reproduce and Share the Licensed Material, in whole or
161 | in part, for NonCommercial purposes only; and
162 |
163 | b. produce, reproduce, and Share Adapted Material for
164 | NonCommercial purposes only.
165 |
166 | 2. Exceptions and Limitations. For the avoidance of doubt, where
167 | Exceptions and Limitations apply to Your use, this Public
168 | License does not apply, and You do not need to comply with
169 | its terms and conditions.
170 |
171 | 3. Term. The term of this Public License is specified in Section
172 | 6(a).
173 |
174 | 4. Media and formats; technical modifications allowed. The
175 | Licensor authorizes You to exercise the Licensed Rights in
176 | all media and formats whether now known or hereafter created,
177 | and to make technical modifications necessary to do so. The
178 | Licensor waives and/or agrees not to assert any right or
179 | authority to forbid You from making technical modifications
180 | necessary to exercise the Licensed Rights, including
181 | technical modifications necessary to circumvent Effective
182 | Technological Measures. For purposes of this Public License,
183 | simply making modifications authorized by this Section 2(a)
184 | (4) never produces Adapted Material.
185 |
186 | 5. Downstream recipients.
187 |
188 | a. Offer from the Licensor -- Licensed Material. Every
189 | recipient of the Licensed Material automatically
190 | receives an offer from the Licensor to exercise the
191 | Licensed Rights under the terms and conditions of this
192 | Public License.
193 |
194 | b. Additional offer from the Licensor -- Adapted Material.
195 | Every recipient of Adapted Material from You
196 | automatically receives an offer from the Licensor to
197 | exercise the Licensed Rights in the Adapted Material
198 | under the conditions of the Adapter's License You apply.
199 |
200 | c. No downstream restrictions. You may not offer or impose
201 | any additional or different terms or conditions on, or
202 | apply any Effective Technological Measures to, the
203 | Licensed Material if doing so restricts exercise of the
204 | Licensed Rights by any recipient of the Licensed
205 | Material.
206 |
207 | 6. No endorsement. Nothing in this Public License constitutes or
208 | may be construed as permission to assert or imply that You
209 | are, or that Your use of the Licensed Material is, connected
210 | with, or sponsored, endorsed, or granted official status by,
211 | the Licensor or others designated to receive attribution as
212 | provided in Section 3(a)(1)(A)(i).
213 |
214 | b. Other rights.
215 |
216 | 1. Moral rights, such as the right of integrity, are not
217 | licensed under this Public License, nor are publicity,
218 | privacy, and/or other similar personality rights; however, to
219 | the extent possible, the Licensor waives and/or agrees not to
220 | assert any such rights held by the Licensor to the limited
221 | extent necessary to allow You to exercise the Licensed
222 | Rights, but not otherwise.
223 |
224 | 2. Patent and trademark rights are not licensed under this
225 | Public License.
226 |
227 | 3. To the extent possible, the Licensor waives any right to
228 | collect royalties from You for the exercise of the Licensed
229 | Rights, whether directly or through a collecting society
230 | under any voluntary or waivable statutory or compulsory
231 | licensing scheme. In all other cases the Licensor expressly
232 | reserves any right to collect such royalties, including when
233 | the Licensed Material is used other than for NonCommercial
234 | purposes.
235 |
236 |
237 | Section 3 -- License Conditions.
238 |
239 | Your exercise of the Licensed Rights is expressly made subject to the
240 | following conditions.
241 |
242 | a. Attribution.
243 |
244 | 1. If You Share the Licensed Material (including in modified
245 | form), You must:
246 |
247 | a. retain the following if it is supplied by the Licensor
248 | with the Licensed Material:
249 |
250 | i. identification of the creator(s) of the Licensed
251 | Material and any others designated to receive
252 | attribution, in any reasonable manner requested by
253 | the Licensor (including by pseudonym if
254 | designated);
255 |
256 | ii. a copyright notice;
257 |
258 | iii. a notice that refers to this Public License;
259 |
260 | iv. a notice that refers to the disclaimer of
261 | warranties;
262 |
263 | v. a URI or hyperlink to the Licensed Material to the
264 | extent reasonably practicable;
265 |
266 | b. indicate if You modified the Licensed Material and
267 | retain an indication of any previous modifications; and
268 |
269 | c. indicate the Licensed Material is licensed under this
270 | Public License, and include the text of, or the URI or
271 | hyperlink to, this Public License.
272 |
273 | 2. You may satisfy the conditions in Section 3(a)(1) in any
274 | reasonable manner based on the medium, means, and context in
275 | which You Share the Licensed Material. For example, it may be
276 | reasonable to satisfy the conditions by providing a URI or
277 | hyperlink to a resource that includes the required
278 | information.
279 | 3. If requested by the Licensor, You must remove any of the
280 | information required by Section 3(a)(1)(A) to the extent
281 | reasonably practicable.
282 |
283 | b. ShareAlike.
284 |
285 | In addition to the conditions in Section 3(a), if You Share
286 | Adapted Material You produce, the following conditions also apply.
287 |
288 | 1. The Adapter's License You apply must be a Creative Commons
289 | license with the same License Elements, this version or
290 | later, or a BY-NC-SA Compatible License.
291 |
292 | 2. You must include the text of, or the URI or hyperlink to, the
293 | Adapter's License You apply. You may satisfy this condition
294 | in any reasonable manner based on the medium, means, and
295 | context in which You Share Adapted Material.
296 |
297 | 3. You may not offer or impose any additional or different terms
298 | or conditions on, or apply any Effective Technological
299 | Measures to, Adapted Material that restrict exercise of the
300 | rights granted under the Adapter's License You apply.
301 |
302 |
303 | Section 4 -- Sui Generis Database Rights.
304 |
305 | Where the Licensed Rights include Sui Generis Database Rights that
306 | apply to Your use of the Licensed Material:
307 |
308 | a. for the avoidance of doubt, Section 2(a)(1) grants You the right
309 | to extract, reuse, reproduce, and Share all or a substantial
310 | portion of the contents of the database for NonCommercial purposes
311 | only;
312 |
313 | b. if You include all or a substantial portion of the database
314 | contents in a database in which You have Sui Generis Database
315 | Rights, then the database in which You have Sui Generis Database
316 | Rights (but not its individual contents) is Adapted Material,
317 | including for purposes of Section 3(b); and
318 |
319 | c. You must comply with the conditions in Section 3(a) if You Share
320 | all or a substantial portion of the contents of the database.
321 |
322 | For the avoidance of doubt, this Section 4 supplements and does not
323 | replace Your obligations under this Public License where the Licensed
324 | Rights include other Copyright and Similar Rights.
325 |
326 |
327 | Section 5 -- Disclaimer of Warranties and Limitation of Liability.
328 |
329 | a. UNLESS OTHERWISE SEPARATELY UNDERTAKEN BY THE LICENSOR, TO THE
330 | EXTENT POSSIBLE, THE LICENSOR OFFERS THE LICENSED MATERIAL AS-IS
331 | AND AS-AVAILABLE, AND MAKES NO REPRESENTATIONS OR WARRANTIES OF
332 | ANY KIND CONCERNING THE LICENSED MATERIAL, WHETHER EXPRESS,
333 | IMPLIED, STATUTORY, OR OTHER. THIS INCLUDES, WITHOUT LIMITATION,
334 | WARRANTIES OF TITLE, MERCHANTABILITY, FITNESS FOR A PARTICULAR
335 | PURPOSE, NON-INFRINGEMENT, ABSENCE OF LATENT OR OTHER DEFECTS,
336 | ACCURACY, OR THE PRESENCE OR ABSENCE OF ERRORS, WHETHER OR NOT
337 | KNOWN OR DISCOVERABLE. WHERE DISCLAIMERS OF WARRANTIES ARE NOT
338 | ALLOWED IN FULL OR IN PART, THIS DISCLAIMER MAY NOT APPLY TO YOU.
339 |
340 | b. TO THE EXTENT POSSIBLE, IN NO EVENT WILL THE LICENSOR BE LIABLE
341 | TO YOU ON ANY LEGAL THEORY (INCLUDING, WITHOUT LIMITATION,
342 | NEGLIGENCE) OR OTHERWISE FOR ANY DIRECT, SPECIAL, INDIRECT,
343 | INCIDENTAL, CONSEQUENTIAL, PUNITIVE, EXEMPLARY, OR OTHER LOSSES,
344 | COSTS, EXPENSES, OR DAMAGES ARISING OUT OF THIS PUBLIC LICENSE OR
345 | USE OF THE LICENSED MATERIAL, EVEN IF THE LICENSOR HAS BEEN
346 | ADVISED OF THE POSSIBILITY OF SUCH LOSSES, COSTS, EXPENSES, OR
347 | DAMAGES. WHERE A LIMITATION OF LIABILITY IS NOT ALLOWED IN FULL OR
348 | IN PART, THIS LIMITATION MAY NOT APPLY TO YOU.
349 |
350 | c. The disclaimer of warranties and limitation of liability provided
351 | above shall be interpreted in a manner that, to the extent
352 | possible, most closely approximates an absolute disclaimer and
353 | waiver of all liability.
354 |
355 |
356 | Section 6 -- Term and Termination.
357 |
358 | a. This Public License applies for the term of the Copyright and
359 | Similar Rights licensed here. However, if You fail to comply with
360 | this Public License, then Your rights under this Public License
361 | terminate automatically.
362 |
363 | b. Where Your right to use the Licensed Material has terminated under
364 | Section 6(a), it reinstates:
365 |
366 | 1. automatically as of the date the violation is cured, provided
367 | it is cured within 30 days of Your discovery of the
368 | violation; or
369 |
370 | 2. upon express reinstatement by the Licensor.
371 |
372 | For the avoidance of doubt, this Section 6(b) does not affect any
373 | right the Licensor may have to seek remedies for Your violations
374 | of this Public License.
375 |
376 | c. For the avoidance of doubt, the Licensor may also offer the
377 | Licensed Material under separate terms or conditions or stop
378 | distributing the Licensed Material at any time; however, doing so
379 | will not terminate this Public License.
380 |
381 | d. Sections 1, 5, 6, 7, and 8 survive termination of this Public
382 | License.
383 |
384 |
385 | Section 7 -- Other Terms and Conditions.
386 |
387 | a. The Licensor shall not be bound by any additional or different
388 | terms or conditions communicated by You unless expressly agreed.
389 |
390 | b. Any arrangements, understandings, or agreements regarding the
391 | Licensed Material not stated herein are separate from and
392 | independent of the terms and conditions of this Public License.
393 |
394 |
395 | Section 8 -- Interpretation.
396 |
397 | a. For the avoidance of doubt, this Public License does not, and
398 | shall not be interpreted to, reduce, limit, restrict, or impose
399 | conditions on any use of the Licensed Material that could lawfully
400 | be made without permission under this Public License.
401 |
402 | b. To the extent possible, if any provision of this Public License is
403 | deemed unenforceable, it shall be automatically reformed to the
404 | minimum extent necessary to make it enforceable. If the provision
405 | cannot be reformed, it shall be severed from this Public License
406 | without affecting the enforceability of the remaining terms and
407 | conditions.
408 |
409 | c. No term or condition of this Public License will be waived and no
410 | failure to comply consented to unless expressly agreed to by the
411 | Licensor.
412 |
413 | d. Nothing in this Public License constitutes or may be interpreted
414 | as a limitation upon, or waiver of, any privileges and immunities
415 | that apply to the Licensor or You, including from the legal
416 | processes of any jurisdiction or authority.
417 |
418 | =======================================================================
419 |
420 | Creative Commons is not a party to its public
421 | licenses. Notwithstanding, Creative Commons may elect to apply one of
422 | its public licenses to material it publishes and in those instances
423 | will be considered the “Licensor.” The text of the Creative Commons
424 | public licenses is dedicated to the public domain under the CC0 Public
425 | Domain Dedication. Except for the limited purpose of indicating that
426 | material is shared under a Creative Commons public license or as
427 | otherwise permitted by the Creative Commons policies published at
428 | creativecommons.org/policies, Creative Commons does not authorize the
429 | use of the trademark "Creative Commons" or any other trademark or logo
430 | of Creative Commons without its prior written consent including,
431 | without limitation, in connection with any unauthorized modifications
432 | to any of its public licenses or any other arrangements,
433 | understandings, or agreements concerning use of licensed material. For
434 | the avoidance of doubt, this paragraph does not form part of the
435 | public licenses.
436 |
437 | Creative Commons may be contacted at creativecommons.org.
438 |
--------------------------------------------------------------------------------