Reupload
This commit is contained in:
commit
f0f6f0c393
25 changed files with 41097 additions and 0 deletions
21
LICENSE
Normal file
21
LICENSE
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2019 Jay
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
121
README.md
Normal file
121
README.md
Normal file
|
@ -0,0 +1,121 @@
|
||||||
|
# Hamspam
|
||||||
|
|
||||||
|
Hamspam is a browser extension that injects visual feedback about suspicious emails into your page.
|
||||||
|
|
||||||
|
## Preview
|
||||||
|
|
||||||
|
Hamspam scans for malware [checkpoints](#checkpoints "checkpoints") and marks all vulnerabilities with color-coded icons:
|
||||||
|
|
||||||
|
| Icon | Severity |
|
||||||
|
|-|-|
|
||||||
|
<img src="./docs/assets/img/pass.png" width="24"> | Low |
|
||||||
|
<img src="./docs/assets/img/warning.png" width="24"> | Medium |
|
||||||
|
<img src="./docs/assets/img/danger.png" width="24"> | High |
|
||||||
|
|
||||||
|
<p align="center"><img alt="" src="./docs/assets/img/preview1.png" width=49%><img alt="" src="./docs/assets/img/preview2.png" width=49%></p>
|
||||||
|
|
||||||
|
## Getting Started
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
|
||||||
|
* Node.js
|
||||||
|
```
|
||||||
|
$ yum install nodejs
|
||||||
|
$ yum install npm
|
||||||
|
```
|
||||||
|
|
||||||
|
* Install npm packages
|
||||||
|
```
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
### Development
|
||||||
|
* Watch for updates to code and compile automatically: `npm run develop`
|
||||||
|
* Build the optimized production: `npm run build`
|
||||||
|
* Run all unit tests: `npm run test`
|
||||||
|
|
||||||
|
### Usage
|
||||||
|
|
||||||
|
1. Create a class that extends AbstractEmail and another class that extends AbstractView.
|
||||||
|
|
||||||
|
2. Define those new classes in [Lib.ts](src/Lib.ts "Lib.ts")
|
||||||
|
```
|
||||||
|
export * from './<Email>';
|
||||||
|
export * from './<EmailView>';
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Add a configuration in [config.yaml](dist/config.yaml "config.yaml")
|
||||||
|
```
|
||||||
|
email:
|
||||||
|
- {name: <Email>, hostname: <email.com>, view: <EmailView>}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### AbstractEmail
|
||||||
|
|
||||||
|
##### Checkpoints
|
||||||
|
|
||||||
|
| Checkpoint | Method | Return | Description |
|
||||||
|
|-|-|-|-|
|
||||||
|
| Sender | `isTrustedSender(<String> sender)` | `bool` | Checks if sender's address is in whitelist |
|
||||||
|
| Received timestamp | `deliveredLateNight(<String> deliveryTime)` | `bool` | Checks if email was received overnight |
|
||||||
|
| Links | `isMaliciousLink(<String> uri)` | `array` | Checks if a link is: <ul> <li>an http url</li> <li>an IP address</li> <li>a non-standard port (https over 80 and http over 443)</li> <li>authentication credentials</li> <li>a shady top-level domain</li> <li>a file path on local computer</li> <li>redirecting to another page</li> <li>containing - or %</li> <li>hosted on a free web hosting provider</li> </ul> |
|
||||||
|
| Attachments | `isSuspiciousFile(<String> filename)` | `bool` | Checks if an attachment is:<ul> <li>an executable file that may contain malicious code</li> <li>script or command file</li> <li>a pdf and MS Office that may contain macro malware</li> </ul> |
|
||||||
|
| Spam words | `findSpamWords(<HTMLElement> test)` | `array` | Searches for every spam word and its start and end position. Built-in text recognition:<ul> <li>converts emoji to text description to detect spam words</li> <li>detects spam words in an image</li> </ul> |
|
||||||
|
|
||||||
|
##### Parsing
|
||||||
|
|
||||||
|
| Method | Return | Description |
|
||||||
|
|-|-|-|
|
||||||
|
| `getBody()` | `object` | Container of email body |
|
||||||
|
| `getSender()` | `object` | Container of sender's email address |
|
||||||
|
| `getSenderString()` | `str` | Sender's email address |
|
||||||
|
| `getDeliveryTime()` | `object` | Container of received timestamp |
|
||||||
|
| `getDeliveryTimeString()` | `str` | Received timestamp |
|
||||||
|
| `getLink()` | `array` | All links |
|
||||||
|
| `getLinkString()` | `array` | All link texts |
|
||||||
|
| `getAttachment()` | `array` | All containers of attachments |
|
||||||
|
| `getAttachmentString()` | `array` | All filenames |
|
||||||
|
|
||||||
|
#### AbstractView
|
||||||
|
|
||||||
|
To use your own icons, override `getIndicator()` and `getIndicatorClass()`.
|
||||||
|
|
||||||
|
| Method | Return | Icon Positon |
|
||||||
|
|-|-|-|
|
||||||
|
| `showIfIsTrustedSender(<HTMLElement> position, <Boolean>isTrustedSender)` | - | Sender's email address |
|
||||||
|
| `showIfDeliveredLateNight(<HTMLElement> position, <Boolean>deliveredLateNight)` | - | Received timestamp |
|
||||||
|
| `showMaliciousLink(<HTMLElement> position, <MaliciousLinkType[]> maliciousLinkType` | - | Each link |
|
||||||
|
| `showSuspiciousFile(<HTMLElement> position, <Boolean> isSuspiciousFile)` | - | Each attachment |
|
||||||
|
| `showSpamWord(<String> triggerWord)` | `str` | Each spam word |
|
||||||
|
|
||||||
|
#### Configuration
|
||||||
|
|
||||||
|
##### email
|
||||||
|
- `name`: child class of AbstractEmail
|
||||||
|
- `hostname`: the last part of the email address that comes after the at sign
|
||||||
|
- `view`: child class of AbstarctView
|
||||||
|
|
||||||
|
##### sender-whitelist
|
||||||
|
Whitelist of email addresses.
|
||||||
|
|
||||||
|
Using regular expressions: `\*@\*.email.com` will whitelist all email addresses ending with @email.com and @sub.email.com
|
||||||
|
|
||||||
|
##### late-delivery
|
||||||
|
- `from`: starting time of overnight hours (default: 300=5am)
|
||||||
|
- `to`: ending time of overnight hours (default: 1320=10pm)
|
||||||
|
|
||||||
|
Increase by 60 every hour after midnight. 0=midnight, 1=0:01am, 30=0:30am, 60=1am, 720=noon, 780=1pm
|
||||||
|
|
||||||
|
##### suspicious-file-extensions
|
||||||
|
|
||||||
|
Blocked attachment file extensions.
|
||||||
|
|
||||||
|
##### spam-words
|
||||||
|
|
||||||
|
Blocked words and phrases.
|
||||||
|
|
||||||
|
Using regular expressions:
|
||||||
|
- `gift( card)?` = gift, gift card
|
||||||
|
|
||||||
|
A blocked term is insensitive to blank spaces:
|
||||||
|
- `sign up` = signup (0 space), sign up (1), sign up (2)
|
8
dist/background.js
vendored
Normal file
8
dist/background.js
vendored
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
var exports = {};
|
||||||
|
|
||||||
|
chrome.browserAction.onClicked.addListener(
|
||||||
|
function (tab) {
|
||||||
|
chrome.tabs.executeScript(tab.id, {
|
||||||
|
file: './js/bundle.js'
|
||||||
|
});
|
||||||
|
});
|
351
dist/config.yaml
vendored
Normal file
351
dist/config.yaml
vendored
Normal file
|
@ -0,0 +1,351 @@
|
||||||
|
---
|
||||||
|
email:
|
||||||
|
- {name: Outlook, hostname: outlook.office.com, view: OutlookView}
|
||||||
|
|
||||||
|
sender-whitelist:
|
||||||
|
- email@example.com
|
||||||
|
- \*@\*.example.com
|
||||||
|
- demo@example.com
|
||||||
|
- admin@example.com
|
||||||
|
|
||||||
|
late-delivery:
|
||||||
|
#60 = 1hr
|
||||||
|
from: 300 #300 = 5hrs = 5am
|
||||||
|
to: 1320 #1320 = 22hrs = 10pm
|
||||||
|
|
||||||
|
suspicious-file-extensions:
|
||||||
|
- ade #ADC Audio File
|
||||||
|
- adp #Access Project (Microsoft)
|
||||||
|
- app #Executable Application
|
||||||
|
- asp #Active Server Page
|
||||||
|
- bas #BASIC Source Code
|
||||||
|
- bat #Bath Processing
|
||||||
|
- cer #Internet Security Certificate File
|
||||||
|
- chm #Complied HTML Help
|
||||||
|
- cla #Java class File
|
||||||
|
- class #Java class File
|
||||||
|
- cmd #DOS CP/M Command File, Command File for Windows NT
|
||||||
|
- cnt #Help file index
|
||||||
|
- com #Command
|
||||||
|
- cpl #Windows Control Panel Extension (Microsoft)
|
||||||
|
- crt #Certificate File
|
||||||
|
- csh #csh Script
|
||||||
|
- der #DER Encoded X509 Certificate File
|
||||||
|
- exe #Executable File
|
||||||
|
- fxp #FoxPro Compiled Source (Microsoft)
|
||||||
|
- gadget #Windows Vista gadget
|
||||||
|
- grp #Microsoft Windows Program Group
|
||||||
|
- hlp #Windows Help Filex
|
||||||
|
- hpj #Project file used to create Windows Help File
|
||||||
|
- hta #Hypertext Application
|
||||||
|
- inf #Information or Setup File
|
||||||
|
- ins #IIS Internet Communications Settings (Microsoft)
|
||||||
|
- isp #IIS Internet Service Provider Settings (Microsoft)
|
||||||
|
- its #Internet Document Set, Internet Translation
|
||||||
|
- jar #Compressed archive file package for Java classes and data
|
||||||
|
- js #JavaScript Source Code
|
||||||
|
- jse #JScript Encoded Script File
|
||||||
|
- ksh #UNIX Shell Script
|
||||||
|
- lnk #Windows Shortcut File
|
||||||
|
- mad #Acces Module Shortcut (Microsoft)
|
||||||
|
- maf #Access (Microsoft)
|
||||||
|
- mag #Access Diagram Shortcut (Microsoft)
|
||||||
|
- mam #Access Macro Shortcut (Microsoft)
|
||||||
|
- maq #Access Query Shortcut (Microsoft)
|
||||||
|
- mar #Access Report SHortcut (Microsoft)
|
||||||
|
- mas #Access Stored Procedures (Microsoft)
|
||||||
|
- mat #Access Table Shortcut (Microsoft)
|
||||||
|
- mau #Media Attachment Unit
|
||||||
|
- mav #Access View Shortcut (Microsoft)
|
||||||
|
- maw #Access Data Access Page (Microsoft)
|
||||||
|
- mcf #MMS Composer File
|
||||||
|
- mda #Access Add-in (Microsoft), MDA Access 2 Workgroup (Microsoft)
|
||||||
|
- mdb #Access Application (Microsoft), MDB Access Database (Microsoft)
|
||||||
|
- mde #Access MDE Database File (Microsoft)
|
||||||
|
- mdt #Access Add-in Data (Microsoft)
|
||||||
|
- mdw #Access Workgroup Information (Microsoft)
|
||||||
|
- mdz #Access Wizard Template (Microsoft)
|
||||||
|
- msc #Microsoft Management Console Snap-in Control File (Microsoft)
|
||||||
|
- msh #Microsoft Shell
|
||||||
|
- msh1 #Microsoft Shell
|
||||||
|
- msh2 #Microsoft Shell
|
||||||
|
- mshxml #Microsoft Shell
|
||||||
|
- msh1xml #Microsoft Shell
|
||||||
|
- msh2xml #Microsoft Shell
|
||||||
|
- msi #Widows Installer File (Microsoft)
|
||||||
|
- msp #Windows Installer Update
|
||||||
|
- mst #Windows SDK Setup Transform Script
|
||||||
|
- ocx #ActiveX Control file
|
||||||
|
- ops #Office Profile Settings File
|
||||||
|
- osd #Application virtualized with Microsoft SoftGrid Sequencer
|
||||||
|
- pcd #Visual Test (Microsoft)
|
||||||
|
- pif #Windows Program Information File (Microsoft)
|
||||||
|
- pl #Perl script language source code
|
||||||
|
- plg #Developer Studio Build Log
|
||||||
|
- prf #Windows System File
|
||||||
|
- prg #Program File
|
||||||
|
- pst #MS Exchange Address Book File, Outlook Personal Folder File (Microsoft)
|
||||||
|
- reg #Registration Information/Key for W95/98, Registry Data File
|
||||||
|
- scf #Windows Explorer Command
|
||||||
|
- scr #Windows Screen Saver
|
||||||
|
- sct #Windows Script Component, Foxpro Screen (Microsoft)
|
||||||
|
- shb #Windows Shourtcut into a Document
|
||||||
|
- shs #Shell Scrap Object File
|
||||||
|
- ps1 #Windows PowerShell
|
||||||
|
- ps1xml #Windows PowerShell
|
||||||
|
- ps2 #Windows PowerShell
|
||||||
|
- ps2xml #Windows PowerShell
|
||||||
|
- psc1 #Windows PowerShell
|
||||||
|
- psc2 #Windows PowerShell
|
||||||
|
- tmp #Temporary File/Folder
|
||||||
|
- url #Internet Location
|
||||||
|
- vb #VBScript File or Any VisualBasic Source
|
||||||
|
- vbe #VBScript Encoded Script File
|
||||||
|
- vbp #Visual Basic project file
|
||||||
|
- vbs #VBScript Script File, Visual Basic for Applications Script
|
||||||
|
- vsmacros #Visual Studio .NET Binary-based Macro Project (Microsoft)
|
||||||
|
- vsw #Visio Workspace File (Microsoft)
|
||||||
|
- ws #Windows Script File
|
||||||
|
- wsc #Windows Script Component
|
||||||
|
- wsf #Windows Script File
|
||||||
|
- wsh #Windows Script Host Settings File
|
||||||
|
- xbap #Silverlight Application Package
|
||||||
|
- xnk #Exchange Public Folder Shortcut
|
||||||
|
- docm #Microsoft Word Macro
|
||||||
|
- doc #Microsoft Word Macro
|
||||||
|
- xlsm #Microsoft Excel Macro
|
||||||
|
- xls #Microsoft Excel Macro
|
||||||
|
- pptm #Microsoft PowerPoint Macro
|
||||||
|
- ppt #Microsoft PowerPoint Macro
|
||||||
|
|
||||||
|
spam-words:
|
||||||
|
- social security number(s)?
|
||||||
|
- SSN
|
||||||
|
- username
|
||||||
|
- bank account number(s)?
|
||||||
|
- maiden name
|
||||||
|
- birth (day|date)
|
||||||
|
- (\W|..)\$ \d+(\.\d+)?
|
||||||
|
- ad
|
||||||
|
- (for )?f r e e
|
||||||
|
- \W\# 1
|
||||||
|
- 100 % (free)?
|
||||||
|
- \d+(.\d+)?( )? % (off|free|satisfied)
|
||||||
|
- all new
|
||||||
|
- bargain
|
||||||
|
- best price
|
||||||
|
- bonus
|
||||||
|
- brand new
|
||||||
|
- cost(s)?
|
||||||
|
- discount
|
||||||
|
- (don't|dont|do not) (delete|hesitate)
|
||||||
|
- marketing
|
||||||
|
- gift (card)?
|
||||||
|
- giv(e|ing)( it)? away
|
||||||
|
- great offer
|
||||||
|
- dollars
|
||||||
|
- incredible deal
|
||||||
|
- (insurance|mortgage) (rate(|s))?
|
||||||
|
- internet
|
||||||
|
- effective
|
||||||
|
- low(er|est) (interest)? (rate(|s))
|
||||||
|
- luxury( car)?
|
||||||
|
- name brand
|
||||||
|
- one hundred percent free
|
||||||
|
- please read
|
||||||
|
- prize(s)?
|
||||||
|
- profit(s)?
|
||||||
|
- sale(s)?
|
||||||
|
- sample
|
||||||
|
- satisfaction
|
||||||
|
- on sale
|
||||||
|
- (best)? rate(s)?
|
||||||
|
- spam
|
||||||
|
- web traffic
|
||||||
|
- (credit)? card(s)? (accepted|(offer(|s))?)?
|
||||||
|
- as seen on
|
||||||
|
- pay (check|stub)
|
||||||
|
- click( below| here| to remove)?
|
||||||
|
- deal
|
||||||
|
- debt
|
||||||
|
- direct email
|
||||||
|
- do it today
|
||||||
|
- form
|
||||||
|
- free (access|instant)
|
||||||
|
- (full)? refund
|
||||||
|
- investment
|
||||||
|
- life( insurance)?
|
||||||
|
- lifetime
|
||||||
|
- (month )?trial offer(s)?
|
||||||
|
- not (junk|spam)
|
||||||
|
- notspam
|
||||||
|
- looking for someone
|
||||||
|
- obligation
|
||||||
|
- opt(-)?in
|
||||||
|
- order(s) (now|shipped|status|today)?
|
||||||
|
- per (day|week)
|
||||||
|
- (cell|mobile|work|home)? phone
|
||||||
|
- presently
|
||||||
|
- price(s)?
|
||||||
|
- print (from signature|out and fax)
|
||||||
|
- priority mail
|
||||||
|
- purchase(s|d)?
|
||||||
|
- (remove|removal) instruction(s)?
|
||||||
|
- serious (only)?
|
||||||
|
- sign up (free today)?
|
||||||
|
- subscribe
|
||||||
|
- work independently
|
||||||
|
- take action (now)?
|
||||||
|
- terms and conditions
|
||||||
|
- unlimited (trial)?
|
||||||
|
- waiting for?
|
||||||
|
- visit (our|their) website
|
||||||
|
- act now (!)?
|
||||||
|
- buy(ing)? (direct)?
|
||||||
|
- call (free|now)
|
||||||
|
- clearance
|
||||||
|
- enclosed
|
||||||
|
- exclusive deal
|
||||||
|
- expire(s)?
|
||||||
|
- get (it|started) now
|
||||||
|
- important information
|
||||||
|
- instant
|
||||||
|
- limited (time)? (offer|only)?
|
||||||
|
- new customers only
|
||||||
|
- now only
|
||||||
|
- once in (a)? lifetime
|
||||||
|
- lift time
|
||||||
|
- one(-)?time
|
||||||
|
- special promotion
|
||||||
|
- supplies (are limited)?
|
||||||
|
- (won't|wont|will not) last
|
||||||
|
- time limited
|
||||||
|
- urgent(ly)?
|
||||||
|
- 4 u
|
||||||
|
- acceptance
|
||||||
|
- bankrupt(cy)?
|
||||||
|
- being a member
|
||||||
|
- boss
|
||||||
|
- bulk email
|
||||||
|
- cancel (at any time)?
|
||||||
|
- certified
|
||||||
|
- chance
|
||||||
|
- cheap
|
||||||
|
- compare rate(|s)
|
||||||
|
- congrat(s|ulations)
|
||||||
|
- cures (baldness)?
|
||||||
|
- dear (friend(|s))
|
||||||
|
- dormant
|
||||||
|
- suspend(ed)?
|
||||||
|
- easy terms
|
||||||
|
- expect to earn
|
||||||
|
- free (info|installation|membership|hosting|(grant )?money)
|
||||||
|
- guarantee(d)?
|
||||||
|
- stranger
|
||||||
|
- info(rmation)? you requested
|
||||||
|
- mass email
|
||||||
|
- member
|
||||||
|
- no (experience|inventory|investment)
|
||||||
|
- opportunity
|
||||||
|
- approved
|
||||||
|
- save ($ |money|for yourself)
|
||||||
|
- thousands
|
||||||
|
- win(ner|ning)?
|
||||||
|
- earn ($ |(extra)? cash |per week )?
|
||||||
|
- extra (cash|income)
|
||||||
|
- fantastic (deal )?
|
||||||
|
- get paid
|
||||||
|
- paid (part|full)time (job|position)
|
||||||
|
- income
|
||||||
|
- make ($ |money)
|
||||||
|
- money (bank|making)
|
||||||
|
- no fee(s)?
|
||||||
|
- no interest(s)?
|
||||||
|
- risk free
|
||||||
|
- vacation
|
||||||
|
- weight (loss)?
|
||||||
|
- work (at|from)? home
|
||||||
|
- beneficiary
|
||||||
|
- beverage
|
||||||
|
- (billing|home|your)? (address(|es)?)
|
||||||
|
- casino
|
||||||
|
- celebrity
|
||||||
|
- legal
|
||||||
|
- diagnostics
|
||||||
|
- hidden
|
||||||
|
- junk
|
||||||
|
- laws
|
||||||
|
- loan(s)?
|
||||||
|
- medicine
|
||||||
|
- meet single(s)?
|
||||||
|
- message contains (disclaimer)?
|
||||||
|
- miracle
|
||||||
|
- nigerian
|
||||||
|
- off( )?shore
|
||||||
|
- online (degree|marketing|pharmacy)
|
||||||
|
- password(s)?
|
||||||
|
- refinance
|
||||||
|
- flexible
|
||||||
|
- reward(ing)?
|
||||||
|
- I tried it
|
||||||
|
- employment
|
||||||
|
- apply (here|now)?
|
||||||
|
- weekly pay
|
||||||
|
- zip code
|
||||||
|
- (We|I) may need (you)?
|
||||||
|
- bank
|
||||||
|
- (first|last) name
|
||||||
|
- email your details to
|
||||||
|
- for kids
|
||||||
|
- IP
|
||||||
|
- satisfactorily
|
||||||
|
- click here
|
||||||
|
- upgrade
|
||||||
|
- in a timely manner
|
||||||
|
- your email account
|
||||||
|
- unknown location
|
||||||
|
- validat(e|ion)
|
||||||
|
- recommend
|
||||||
|
- 100%
|
||||||
|
- legit
|
||||||
|
- takes less than
|
||||||
|
- email with your details
|
||||||
|
- $ \d+ weekly
|
||||||
|
- $ \d+ per week
|
||||||
|
- compensated
|
||||||
|
- 18\+
|
||||||
|
- anyone can
|
||||||
|
- without affecting
|
||||||
|
- (doesn't|does not|doesnt) affect
|
||||||
|
- extra benefit(s)?
|
||||||
|
- more details (here)?
|
||||||
|
- in touch with you
|
||||||
|
- recruit(ing|ment)?
|
||||||
|
- cool cash
|
||||||
|
- read more
|
||||||
|
- position available
|
||||||
|
- survey|questionnaire
|
||||||
|
- shopper(s)?
|
||||||
|
- (part|full)(-)?time( job)?
|
||||||
|
- fear
|
||||||
|
- (will not|won't|wont) affect
|
||||||
|
- (make|earn) up to
|
||||||
|
- what can you expect
|
||||||
|
- security (alert)?
|
||||||
|
- administrative
|
||||||
|
- receiv(e|ing) payment
|
||||||
|
- information below
|
||||||
|
- instructed to
|
||||||
|
- share with us
|
||||||
|
- to register
|
||||||
|
- you will only
|
||||||
|
- error(s)?
|
||||||
|
- you will get $ d+
|
||||||
|
- available to all
|
||||||
|
- personal assistan(t|ce)
|
||||||
|
- you (\'ve|have) won
|
||||||
|
- ATTN|attention
|
||||||
|
- confirm(ation)?
|
||||||
|
- DRIVE N EARNS PROGRAM
|
||||||
|
- URGENT MESSAGE FROM THE UNIVERSITY
|
||||||
|
- SF ALERT
|
103
dist/css/index.css
vendored
Normal file
103
dist/css/index.css
vendored
Normal file
|
@ -0,0 +1,103 @@
|
||||||
|
@import url("https://maxcdn.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css");
|
||||||
|
|
||||||
|
/** removes greybox around button **/
|
||||||
|
button.hamspam {
|
||||||
|
border: none;
|
||||||
|
outline: none;
|
||||||
|
background: transparent;
|
||||||
|
z-index: 2147483647 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
button.hamspam > svg:nth-child(1) {
|
||||||
|
width: 27px;
|
||||||
|
height: 27px;
|
||||||
|
margin: 3.5px;
|
||||||
|
padding: 0;
|
||||||
|
stroke: #000;
|
||||||
|
stroke-width: 1px;
|
||||||
|
stroke-linejoin: round;
|
||||||
|
border: 1.5px solid #666;
|
||||||
|
border-radius: 5px;
|
||||||
|
background: transparent;
|
||||||
|
vertical-align: middle;
|
||||||
|
display: inline-block;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
button.hamspam > svg:nth-child(1).hamspam-secure {
|
||||||
|
fill: #0071bc;
|
||||||
|
background: #4dabf7;
|
||||||
|
padding: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
button.hamspam > svg:nth-child(1).hamspam-warning {
|
||||||
|
fill: #fdb81e;
|
||||||
|
background: #f9c642;
|
||||||
|
padding: 0.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
button.hamspam > svg:nth-child(1).hamspam-critical {
|
||||||
|
fill: #ff454d;
|
||||||
|
background: #ff6970;
|
||||||
|
border-radius:100%;
|
||||||
|
padding: 2px;
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.popover {
|
||||||
|
font-family: "Helvetica", "Arial", sans-serif !important;
|
||||||
|
border-radius: 3px;
|
||||||
|
user-select: text;
|
||||||
|
width: max-content !important;
|
||||||
|
max-width: 22vw !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.popover > * {
|
||||||
|
color: #333;
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
|
.popover-header {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
background: #f9f9f9;
|
||||||
|
border-bottom: .5px solid #ccc;
|
||||||
|
padding: 4px 5px 2px 6px;
|
||||||
|
margin-bottom: -3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.popover-body {
|
||||||
|
font-size: 12px;
|
||||||
|
padding: 5px 5px 4px 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.popover-body > * > ul {
|
||||||
|
margin: 0;
|
||||||
|
padding-inline-start: 1em;
|
||||||
|
list-style-type: circle;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bs-popover-auto, .bs-popover-right, .bs-popover-bottom, .bs-popover-left, .bs-popover-top {
|
||||||
|
border: 0.75px solid #ccc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bs-popover-right > .arrow::before {
|
||||||
|
border-right-color: #555;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bs-popover-bottom > .arrow::before {
|
||||||
|
border-bottom-color: #555;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bs-popover-left > .arrow::before {
|
||||||
|
border-left-color: #555;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bs-popover-top > .arrow::before {
|
||||||
|
border-top-color: #555;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shadow-sm {
|
||||||
|
box-shadow: 0 0 0.08rem rgba(0, 0, 0, 1.0) !important;
|
||||||
|
}
|
34634
dist/js/bundle.js
vendored
Normal file
34634
dist/js/bundle.js
vendored
Normal file
File diff suppressed because one or more lines are too long
26
dist/manifest.json
vendored
Normal file
26
dist/manifest.json
vendored
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
{
|
||||||
|
"manifest_version": 2,
|
||||||
|
"name": "Hamspam",
|
||||||
|
"version": "1.0",
|
||||||
|
|
||||||
|
"author": "Jay",
|
||||||
|
"description": "Provide visual feedback about spamminess of emails",
|
||||||
|
"homepage_url": "https://github.com/7ae/hamspam",
|
||||||
|
"background": {
|
||||||
|
"scripts": [
|
||||||
|
"background.js"
|
||||||
|
],
|
||||||
|
"persistent": true
|
||||||
|
},
|
||||||
|
"browser_action": {
|
||||||
|
"default_title": "Run Hamspam"
|
||||||
|
},
|
||||||
|
"permissions": [
|
||||||
|
"https://*/",
|
||||||
|
"http://*/",
|
||||||
|
"contextMenus"
|
||||||
|
],
|
||||||
|
"web_accessible_resources": [
|
||||||
|
"/*"
|
||||||
|
]
|
||||||
|
}
|
BIN
docs/assets/img/danger.png
Normal file
BIN
docs/assets/img/danger.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 687 B |
BIN
docs/assets/img/pass.png
Normal file
BIN
docs/assets/img/pass.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 615 B |
BIN
docs/assets/img/preview1.png
Normal file
BIN
docs/assets/img/preview1.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 74 KiB |
BIN
docs/assets/img/preview2.png
Normal file
BIN
docs/assets/img/preview2.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 83 KiB |
BIN
docs/assets/img/warning.png
Normal file
BIN
docs/assets/img/warning.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 685 B |
4757
package-lock.json
generated
Normal file
4757
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
27
package.json
Normal file
27
package.json
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
{
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/chrome": "0.0.95",
|
||||||
|
"@types/jquery": "^3.3.32",
|
||||||
|
"@types/js-yaml": "^3.12.1",
|
||||||
|
"@types/node": "^12.11.6",
|
||||||
|
"@types/tesseract.js": "0.0.2",
|
||||||
|
"js-yaml": "^3.13.1",
|
||||||
|
"ts-loader": "^6.2.0",
|
||||||
|
"typescript": "^3.6.4",
|
||||||
|
"webpack": "^4.41.1",
|
||||||
|
"webpack-cli": "^3.3.9"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@types/bootstrap": "^4.3.1",
|
||||||
|
"@types/uuid": "^8.0.0",
|
||||||
|
"bootstrap": "^4.4.1",
|
||||||
|
"jquery": "^3.4.1",
|
||||||
|
"tesseract.js": "^2.0.2",
|
||||||
|
"uuid": "^8.1.0"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"test": "echo 'test not implemented'",
|
||||||
|
"develop": "webpack --mode development --watch",
|
||||||
|
"build": "webpack --mode production"
|
||||||
|
}
|
||||||
|
}
|
374
src/AbstractEmail.ts
Normal file
374
src/AbstractEmail.ts
Normal file
|
@ -0,0 +1,374 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
import { Config } from './Config';
|
||||||
|
const { createWorker, createScheduler } = require('tesseract.js');
|
||||||
|
|
||||||
|
/*
|
||||||
|
* AbstractEmail - scan user's email with a set of predetermined rules created based on common spam attributes
|
||||||
|
*
|
||||||
|
* @version 1.0 2019-10
|
||||||
|
*/
|
||||||
|
|
||||||
|
export abstract class AbstractEmail {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor for objects of AbstractEmail
|
||||||
|
* @constructor
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
public constructor() { }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check whether sender is whitelisted
|
||||||
|
*
|
||||||
|
* @param {string} sender an email address
|
||||||
|
* @return {boolean} true if whitelist includes sender;
|
||||||
|
* false otherwise.
|
||||||
|
*/
|
||||||
|
public isTrustedSender(sender: string): boolean {
|
||||||
|
const whitelist = Config.getInstance().getConfigurationOf('sender-whitelist');
|
||||||
|
if (whitelist !== undefined && whitelist.length === 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
for (let whitelistItem of whitelist) {
|
||||||
|
// Wildcard * matches any number of characters pattern
|
||||||
|
var regex = new RegExp('^' + whitelistItem.replace(/[+?^${}()|[\]\\]/ig, '\\$&').replace('*', '.*') + '$');
|
||||||
|
if (regex.test(sender)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check whether email has been sent late at night
|
||||||
|
*
|
||||||
|
* @param {string} deliveryTime time
|
||||||
|
* @throws {Error} if delivery time is an invalid date format
|
||||||
|
* exception occurred
|
||||||
|
* @return {boolean} true if delivery time is not late at night;
|
||||||
|
* false otherwise.
|
||||||
|
*/
|
||||||
|
public deliveredLateNight(deliveryTime: string): boolean {
|
||||||
|
var date = new Date(deliveryTime);
|
||||||
|
if (isNaN(date.getTime())) {
|
||||||
|
// Leading zero makes invalid date formats valid
|
||||||
|
date = new Date('0' + deliveryTime);
|
||||||
|
}
|
||||||
|
// Use 24 hour format
|
||||||
|
var hours = date.getHours();
|
||||||
|
var minutes = date.getMinutes();
|
||||||
|
if (isNaN(hours) || isNaN(minutes)) {
|
||||||
|
throw new Error("delivery datetime is an invalid format");
|
||||||
|
}
|
||||||
|
|
||||||
|
const deliveryTimeInMinute = (hours * 60) + minutes;
|
||||||
|
const lateDeliveryFrom = Config.getInstance().getConfigurationOf('late-delivery').from;
|
||||||
|
const lateDeliveryTo = Config.getInstance().getConfigurationOf('late-delivery').to;
|
||||||
|
if (deliveryTimeInMinute < lateDeliveryFrom || deliveryTimeInMinute > lateDeliveryTo) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check whether a link is malicious
|
||||||
|
*
|
||||||
|
* @param {string} uri link address
|
||||||
|
* @return {MaliciousLinkType[]} zero or more rules link address falls under
|
||||||
|
*/
|
||||||
|
public isMaliciousLink(uri: string): AbstractEmail.MaliciousLinkType[] {
|
||||||
|
var result: AbstractEmail.MaliciousLinkType[] = [];
|
||||||
|
|
||||||
|
var anchor = document.createElement('a');
|
||||||
|
anchor.href = uri;
|
||||||
|
// Insecure connection
|
||||||
|
if (anchor.protocol === 'http:') {
|
||||||
|
result.push(AbstractEmail.MaliciousLinkType.INSECURE_CONNECTION);
|
||||||
|
}
|
||||||
|
// Contain dash or %
|
||||||
|
if (anchor.hostname.indexOf('-' || '%') !== -1) {
|
||||||
|
result.push(AbstractEmail.MaliciousLinkType.SPECIALCHAR);
|
||||||
|
}
|
||||||
|
// Non-standard port number like https over 80 or http over 443
|
||||||
|
if (anchor.port !== '' && anchor.protocol !== '') {
|
||||||
|
if ((anchor.protocol === 'https:' && anchor.port === '80') || (anchor.protocol === 'http:' && anchor.port === '443')) {
|
||||||
|
result.push(AbstractEmail.MaliciousLinkType.NONSTANDARD_PORT);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Is ip address
|
||||||
|
if (/^\d+\.\d+\.\d+\.\d+$/g.test(anchor.hostname)) {
|
||||||
|
result.push(AbstractEmail.MaliciousLinkType.IP_ADDRESS);
|
||||||
|
}
|
||||||
|
// Contain authentication credentials
|
||||||
|
if (/.*\:\/\/.*@.*/g.test(anchor.href)) {
|
||||||
|
result.push(AbstractEmail.MaliciousLinkType.AUTHENTICATION_CREDENTIAL);
|
||||||
|
}
|
||||||
|
// Prefixed with file://
|
||||||
|
if (anchor.protocol === 'file:') {
|
||||||
|
result.push(AbstractEmail.MaliciousLinkType.FILE);
|
||||||
|
}
|
||||||
|
// Is shady top-level domain
|
||||||
|
if (['work', 'live', 'buzz', 'tk', 'fit', 'top', 'cn', 'rest'].indexOf(anchor.hostname.split('.').pop()) > -1) {
|
||||||
|
result.push(AbstractEmail.MaliciousLinkType.TOP_LEVEL_DOMAIN);
|
||||||
|
}
|
||||||
|
// Hosted on free web hosting providers
|
||||||
|
for (let webHostingService of ['weebly.com', 'cognitoforms.com', '0000webhostapp.com', 'jigsy.com', 'jotform.com']) {
|
||||||
|
if (anchor.hostname.includes(webHostingService)) {
|
||||||
|
result.push(AbstractEmail.MaliciousLinkType.FREE_HOSTING);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Redirect to another page, such as shortened url
|
||||||
|
var request = new XMLHttpRequest();
|
||||||
|
try {
|
||||||
|
request.withCredentials = true;
|
||||||
|
request.onreadystatechange = function () {
|
||||||
|
if (request.readyState === 4 && request.status === 200) {
|
||||||
|
if (anchor.href !== request.responseURL) {
|
||||||
|
result.push(AbstractEmail.MaliciousLinkType.REDIRECT);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
request.open('HEAD', anchor.href, false);
|
||||||
|
request.send();
|
||||||
|
} catch (e) {
|
||||||
|
console.log(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (anchor.textContent !== '' && result.length === 0) {
|
||||||
|
// Recursively tests a generated link that sets href to link text
|
||||||
|
this.isMaliciousLink(anchor.textContent);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('10');
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check whether attachment file is suspicious
|
||||||
|
*
|
||||||
|
* @param {string} filename file name
|
||||||
|
* @return {boolean} true if attachment is suspicious;
|
||||||
|
* false otherwise.
|
||||||
|
*/
|
||||||
|
public isSuspiciousFile(filename: string): boolean {
|
||||||
|
const suspiciousFileExtensions = Config.getInstance().getConfigurationOf('suspicious-file-extensions');
|
||||||
|
if (suspiciousFileExtensions === undefined) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (suspiciousFileExtensions.indexOf(filename.split('.').pop()) >= 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check whether email contains spam words
|
||||||
|
*
|
||||||
|
* @param {HTMLElement} test email body
|
||||||
|
* @return {Array<[number, string]>} pairs of spam word and character position
|
||||||
|
*/
|
||||||
|
public findSpamWords(test: HTMLElement): Array<[number, string]> {
|
||||||
|
const emojiToString = {
|
||||||
|
'🅰️': 'a',
|
||||||
|
'🆎': 'ab',
|
||||||
|
'🏧': 'atm',
|
||||||
|
'🅱️': 'b',
|
||||||
|
'©': 'c',
|
||||||
|
'💳': 'card',
|
||||||
|
'🆑': 'cl',
|
||||||
|
'🆒': 'cool',
|
||||||
|
'🆓': 'free',
|
||||||
|
'🗽': 'free',
|
||||||
|
'ℹ️': 'info',
|
||||||
|
'🆔': 'id',
|
||||||
|
'🃏': 'j',
|
||||||
|
'🆕': 'new',
|
||||||
|
'🆖': 'ng',
|
||||||
|
'⛔': 'no',
|
||||||
|
'Ⓜ️': 'm',
|
||||||
|
'🅾️': 'o',
|
||||||
|
'🆗': 'ok',
|
||||||
|
'🅿️': 'p',
|
||||||
|
'🚫': 'no',
|
||||||
|
'®': 'r',
|
||||||
|
'🎰': 'casino',
|
||||||
|
'🆘': 'sos',
|
||||||
|
'🎫': 'ticket',
|
||||||
|
'🎟': 'ticket',
|
||||||
|
'💲': '$',
|
||||||
|
'💰': '$',
|
||||||
|
'💸': '$',
|
||||||
|
'💵': '$',
|
||||||
|
'🆙': 'up',
|
||||||
|
'🆚': 'vs',
|
||||||
|
'🚾': 'wc',
|
||||||
|
'❌': 'x',
|
||||||
|
'❎': 'x',
|
||||||
|
'💤': 'z',
|
||||||
|
'#️⃣': '#',
|
||||||
|
'*️⃣': '*',
|
||||||
|
'0️⃣': 0,
|
||||||
|
'1️⃣': 1,
|
||||||
|
'2️⃣': 2,
|
||||||
|
'3️⃣': 3,
|
||||||
|
'4️⃣': 4,
|
||||||
|
'5️⃣': 5,
|
||||||
|
'6️⃣': 6,
|
||||||
|
'7️⃣': 7,
|
||||||
|
'8️⃣': 8,
|
||||||
|
'🎱': 8,
|
||||||
|
'9️⃣': 9,
|
||||||
|
'🔟': 10,
|
||||||
|
'❓': '?',
|
||||||
|
'❔': '?',
|
||||||
|
'❗️': '!',
|
||||||
|
'❕': '!',
|
||||||
|
'‼': '!!',
|
||||||
|
'⁉': '!?',
|
||||||
|
'〰': '~',
|
||||||
|
'➕': '+',
|
||||||
|
'➖': '-',
|
||||||
|
};
|
||||||
|
// Make a deep copy of test string and converts emojis to string
|
||||||
|
var regex = new RegExp('(\\' + Object.keys(emojiToString).join('|\\') + ')', 'g');
|
||||||
|
var testString: string = test.innerHTML.replace(regex, function (match: string) { return emojiToString[match]; });
|
||||||
|
|
||||||
|
// Convert test string to html
|
||||||
|
var span: Element = document.createElement('span');
|
||||||
|
var innerSpan: Element = document.createElement('span');
|
||||||
|
span.appendChild(innerSpan);
|
||||||
|
// Add test string within inner span
|
||||||
|
innerSpan.innerHTML = testString;
|
||||||
|
var htmlObj = span.firstChild;
|
||||||
|
// Stringfy and tests outer span
|
||||||
|
for (let element of <Element[]><unknown>htmlObj.parentElement.querySelectorAll(`button.hamspam`)) {
|
||||||
|
// Replace all characters in indicator tag and internal tags with spaces
|
||||||
|
element.outerHTML = element.outerHTML.replace(/./g, ' ');
|
||||||
|
}
|
||||||
|
testString = (<Element>htmlObj).innerHTML;
|
||||||
|
|
||||||
|
// Store spam word and its character position
|
||||||
|
var tuple: Array<[number, string]> = new Array<[number, string]>();
|
||||||
|
for (let spamWord of Config.getInstance().getConfigurationOf('spam-words')) {
|
||||||
|
// Match word in paragraphs excluding html tags and attribute
|
||||||
|
regex = new RegExp('\\b(' + spamWord.replace(' ', '(\\s+)?') + '(?![^<>]*>))\\b', 'gmi');
|
||||||
|
var match: RegExpExecArray;
|
||||||
|
while ((match = regex.exec(testString)) != null) {
|
||||||
|
tuple.push([match.index, match[0]]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Return 2-tuple of position sorted in ascending order and the spam words
|
||||||
|
return tuple.sort((num1, num2) => num1[0] - num2[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return container of email body
|
||||||
|
*
|
||||||
|
* @return {HTMLElement} container of email body
|
||||||
|
*/
|
||||||
|
public abstract getBody(): HTMLElement;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return container of sender email address
|
||||||
|
*
|
||||||
|
* @return {HTMLElement} container of sender email addresss
|
||||||
|
*/
|
||||||
|
public abstract getSender(): HTMLElement;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return string of sender email address
|
||||||
|
*
|
||||||
|
* @return {string} sender email addresss
|
||||||
|
*/
|
||||||
|
public abstract getSenderString(): string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return container of delivery time
|
||||||
|
*
|
||||||
|
* @return {HTMLElement} container of delivery time
|
||||||
|
*/
|
||||||
|
public abstract getDeliveryTime(): HTMLElement;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return string of delivery time
|
||||||
|
*
|
||||||
|
* @return {string} delivery time
|
||||||
|
*/
|
||||||
|
public abstract getDeliveryTimeString(): string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return an array of all links in email
|
||||||
|
*
|
||||||
|
* @return {Array<HTMLAnchorElement>} array of anchor tags
|
||||||
|
*/
|
||||||
|
public abstract getLink(): Array<HTMLAnchorElement>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return an array of all link text in email
|
||||||
|
*
|
||||||
|
* @return {Array<string>} array of link text
|
||||||
|
*/
|
||||||
|
public abstract getLinkString(): Array<string>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return an array of containers of all attachments
|
||||||
|
*
|
||||||
|
* @return {Array<HTMLElement>} array of containers of attachments
|
||||||
|
*/
|
||||||
|
public abstract getAttachment(): Array<HTMLElement>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return an array of attachment file names
|
||||||
|
*
|
||||||
|
* @return {Array<string>} array of attachment file names
|
||||||
|
*/
|
||||||
|
public abstract getAttachmentString(): Array<string>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Recognize text in images
|
||||||
|
*
|
||||||
|
* @param {Array<string>} imageSources image sources
|
||||||
|
* @return {Promise<object>} promise object with the extracted text from images
|
||||||
|
*/
|
||||||
|
public async imageToString(imageSources: Array<string>): Promise<object> {
|
||||||
|
const scheduler = createScheduler();
|
||||||
|
// Create workers
|
||||||
|
var numWorkers = 3;
|
||||||
|
while (numWorkers-- > 0) {
|
||||||
|
var worker = createWorker();
|
||||||
|
await worker.load();
|
||||||
|
await worker.loadLanguage('eng');
|
||||||
|
await worker.initialize('eng');
|
||||||
|
scheduler.addWorker(worker);
|
||||||
|
}
|
||||||
|
// Print number of workers
|
||||||
|
//console.log(scheduler.getNumWorkers());
|
||||||
|
|
||||||
|
const results = await Promise.all(imageSources.map((oneImageSource) => (
|
||||||
|
scheduler.addJob('recognize', oneImageSource)
|
||||||
|
)));
|
||||||
|
await scheduler.terminate();
|
||||||
|
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MaliciousLinkType - enum class for malicious link type
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
export namespace AbstractEmail {
|
||||||
|
export enum MaliciousLinkType {
|
||||||
|
INSECURE_CONNECTION,
|
||||||
|
SPECIALCHAR,
|
||||||
|
NONSTANDARD_PORT,
|
||||||
|
IP_ADDRESS,
|
||||||
|
AUTHENTICATION_CREDENTIAL,
|
||||||
|
FILE,
|
||||||
|
REDIRECT,
|
||||||
|
TOP_LEVEL_DOMAIN,
|
||||||
|
FREE_HOSTING
|
||||||
|
}
|
||||||
|
}
|
250
src/AbstractView.ts
Normal file
250
src/AbstractView.ts
Normal file
|
@ -0,0 +1,250 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
import * as $ from 'jquery';
|
||||||
|
import 'bootstrap';
|
||||||
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
|
import { AbstractEmail } from './AbstractEmail';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AbstractView - inject various indicators into user's page depending on security level
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
export class AbstractView {
|
||||||
|
public static readonly CLASS: string = 'hamspam';
|
||||||
|
public static readonly BR: string = '<br aria-hidden="true">';
|
||||||
|
public static readonly INDICATOR_SECURE: string = '<svg height="24" viewBox="0 0 24 24" width="24"><path d="M19 3H5c-1.11 0-2 .9-2 2v14c0 1.1.89 2 2 2h14c1.11 0 2-.9 2-2V5c0-1.1-.89-2-2-2zm-9 14l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z"/></svg>';
|
||||||
|
public static readonly INDICATOR_WARNING: string = '<svg height="24" viewBox="0 0 24 24" width="24"><path d="M4.47 21h15.06c1.54 0 2.5-1.67 1.73-3L13.73 4.99c-.77-1.33-2.69-1.33-3.46 0L2.74 18c-.77 1.33.19 3 1.73 3zM12 14c-.55 0-1-.45-1-1v-2c0-.55.45-1 1-1s1 .45 1 1v2c0 .55-.45 1-1 1zm1 4h-2v-2h2v2z"/></svg>';
|
||||||
|
public static readonly INDICATOR_CRITICAL: string = '<svg height="24" viewBox="0 0 24 24" width="24"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 11c-.55 0-1-.45-1-1V8c0-.55.45-1 1-1s1 .45 1 1v4c0 .55-.45 1-1 1zm1 4h-2v-2h2v2z"/></svg>';
|
||||||
|
public static readonly SENDER_SECURE_TITLE: string = 'Whitelisted Sender';
|
||||||
|
public static readonly SENDER_SECURE_DESCRIPTION: string = 'Sender is in your email address whitelist.';
|
||||||
|
public static readonly SENDER_WARNING_TITLE: string = 'Non-Whitelisted Sender';
|
||||||
|
public static readonly SENDER_WARNING_DESCRIPTION: string = `Sender is not in your email address whitelist.`;
|
||||||
|
public static readonly DELIVERY_TIME_CRITICAL_TITLE: string = 'Night Owl';
|
||||||
|
public static readonly DELIVERY_TIME_CRITICAL_DESCRIPTION: string = 'Email received during the late night hours is usually sent from other countries or marketing automation.';
|
||||||
|
public static readonly DELIVERY_TIME_SECURE_TITLE: string = 'Morning Lark';
|
||||||
|
public static readonly DELIVERY_TIME_SECURE_DESCRIPTION: string = `Email was received during the daytime in your ${AbstractView.BR} local time zone.`;
|
||||||
|
public static readonly LINK_SECURE_TITLE: string = 'Safe Link';
|
||||||
|
public static readonly LINK_SECURE_DESCRIPTION: string = 'Link passed all tests and appears to be safe.';
|
||||||
|
public static readonly LINK_CRITICAL_TITLE: string = 'Unsafe Link';
|
||||||
|
public static readonly LINK_CRITICAL_DESCRIPTION: {} = {
|
||||||
|
9999: 'Link may be unsafe because:',
|
||||||
|
[AbstractEmail.MaliciousLinkType.AUTHENTICATION_CREDENTIAL]: 'It contains authentication credentials',
|
||||||
|
[AbstractEmail.MaliciousLinkType.FILE]: 'It is a path to files within your computer',
|
||||||
|
[AbstractEmail.MaliciousLinkType.INSECURE_CONNECTION]: 'It uses the insecure HTTP server connection',
|
||||||
|
[AbstractEmail.MaliciousLinkType.IP_ADDRESS]: 'It is IP address',
|
||||||
|
[AbstractEmail.MaliciousLinkType.NONSTANDARD_PORT]: 'It runs on non-standard port number like HTTPS over 80 or HTTP over 443',
|
||||||
|
[AbstractEmail.MaliciousLinkType.REDIRECT]: 'It redirects to another page',
|
||||||
|
[AbstractEmail.MaliciousLinkType.SPECIALCHAR]: 'It has special characters in the domain name of the URL',
|
||||||
|
[AbstractEmail.MaliciousLinkType.TOP_LEVEL_DOMAIN]: 'Top-level domain is shady',
|
||||||
|
[AbstractEmail.MaliciousLinkType.FREE_HOSTING]: 'It is hosted on free web hosting providers'
|
||||||
|
};
|
||||||
|
public static readonly FILE_SECURE_TITLE: string = 'Safe File';
|
||||||
|
public static readonly FILE_SECURE_DESCRIPTION: string = 'File passed all tests and appears to be safe';
|
||||||
|
public static readonly FILE_WARNING_TITLE: string = 'Suspicious File';
|
||||||
|
public static readonly FILE_WARNING_DESCRIPTION: string = `File may contain macro malware that starts ${AbstractView.BR} automatically to infect your computer without ${AbstractView.BR} your knowledge.`;
|
||||||
|
public static readonly KEYWORD_CRITICAL_TITLE: string = 'Spam Trigger Word';
|
||||||
|
public static KEYWORD_CRITICAL_DESCRIPTION(triggerWord): string {
|
||||||
|
return `The keyword or phrase '<i>${triggerWord}</i>' is often used in spam emails.`;
|
||||||
|
};
|
||||||
|
private popoverIds: string[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor for objects of AbstractView
|
||||||
|
* @constructor
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
constructor() {
|
||||||
|
// Initalize array of unique popover ids
|
||||||
|
this.popoverIds = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Draw indicator with descriptive text
|
||||||
|
*
|
||||||
|
* @param {AbstractView.SecurityLevel} level a security level
|
||||||
|
* @param {string} title tooltip title
|
||||||
|
* @param {string} message tooltip message
|
||||||
|
* @param {Element} [position] tooltip location
|
||||||
|
* @return {string} indicator to inject into page
|
||||||
|
*/
|
||||||
|
public showMessage(level: AbstractView.SecurityLevel, title: string, message: string, position?: Element): string {
|
||||||
|
// Create button
|
||||||
|
var button: HTMLButtonElement = document.createElement('button');
|
||||||
|
button.classList.add(AbstractView.CLASS);
|
||||||
|
|
||||||
|
// Create indicator in button
|
||||||
|
button.innerHTML = this.getIndicator(level);
|
||||||
|
|
||||||
|
// Add styles to indicator
|
||||||
|
(<HTMLElement>button.firstChild).classList.add(this.getIndicatorClass(level));
|
||||||
|
|
||||||
|
// Generate unique id for this popover to distinguish from other popovers on page
|
||||||
|
var uuid = '-' + uuidv4();
|
||||||
|
this.popoverIds.push(uuid);
|
||||||
|
|
||||||
|
// Create tooltip
|
||||||
|
$(button).attr({'data-toggle': 'popover' + uuid, 'onclick': 'return false;', 'data-trigger': 'hover'});
|
||||||
|
$(button).attr('role', 'tooltip');
|
||||||
|
|
||||||
|
// Allow screen readers to announce popover content without mouse hover
|
||||||
|
// TODO: test with screen readers
|
||||||
|
$(button).attr({'aria-describedby': 'description' + uuid});
|
||||||
|
$(`<div class="sr-only" id="description${uuid}"> ${title}. ${message}<div>`).appendTo((<HTMLElement>button.firstChild));
|
||||||
|
|
||||||
|
$(() => {
|
||||||
|
var popover = $('[data-toggle="popover' + uuid + '"]').popover(<any>{
|
||||||
|
title: title,
|
||||||
|
content: message,
|
||||||
|
placement: 'left',
|
||||||
|
html: true,
|
||||||
|
animation: true,
|
||||||
|
// Keep the text of the popover open on mouseover by putting it within popover container
|
||||||
|
container: <any>$('[data-toggle="popover' + uuid + '"]'),
|
||||||
|
template: '<div class="popover shadow-sm" role="tooltip"><div class="arrow"></div><div class="popover-header"></div><div class="popover-body"></div></div>',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Stop the event bubbling on tooltip click
|
||||||
|
$(button).on('click keyup', function(e) {
|
||||||
|
e.stopPropagation();
|
||||||
|
e.preventDefault();
|
||||||
|
});
|
||||||
|
|
||||||
|
if (position) {
|
||||||
|
position.prepend(button);
|
||||||
|
} else {
|
||||||
|
return button.outerHTML;
|
||||||
|
}
|
||||||
|
button.parentElement.style.display = 'block';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Draw indicator near sender email address
|
||||||
|
*
|
||||||
|
* @param {HTMLElement} position character position to inject indicator into
|
||||||
|
* @param {boolean} isTrustedSender whether or not sender email address is trusted
|
||||||
|
*/
|
||||||
|
public showIfIsTrustedSender(position: HTMLElement, isTrustedSender: boolean): void {
|
||||||
|
if (isTrustedSender) {
|
||||||
|
this.showMessage(AbstractView.SecurityLevel.SECURE, AbstractView.SENDER_SECURE_TITLE, AbstractView.SENDER_SECURE_DESCRIPTION, position);
|
||||||
|
} else {
|
||||||
|
this.showMessage(AbstractView.SecurityLevel.WARNING, AbstractView.SENDER_WARNING_TITLE, AbstractView.SENDER_WARNING_DESCRIPTION, position);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Draw indicator near email delivery time
|
||||||
|
*
|
||||||
|
* @param {HTMLElement} position character position to inject indicator into
|
||||||
|
* @param {boolean} deliveredLateNight whether or not email has been delivered late at night
|
||||||
|
*/
|
||||||
|
public showIfDeliveredLateNight(position: HTMLElement, deliveredLateNight: boolean): void {
|
||||||
|
if (deliveredLateNight) {
|
||||||
|
this.showMessage(AbstractView.SecurityLevel.CRITICAL, AbstractView.DELIVERY_TIME_CRITICAL_TITLE, AbstractView.DELIVERY_TIME_CRITICAL_DESCRIPTION, position);
|
||||||
|
} else {
|
||||||
|
this.showMessage(AbstractView.SecurityLevel.SECURE, AbstractView.DELIVERY_TIME_SECURE_TITLE, AbstractView.DELIVERY_TIME_SECURE_DESCRIPTION, position);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Draw indicator near links
|
||||||
|
*
|
||||||
|
* @param {HTMLElement} position character position to inject indicator into
|
||||||
|
* @param {AbstractEmail.MaliciousLinkType[]} maliciousLinkType why link is malicious
|
||||||
|
*/
|
||||||
|
public showMaliciousLink(position: HTMLElement, maliciousLinkType: AbstractEmail.MaliciousLinkType[]): void {
|
||||||
|
if (maliciousLinkType.length === 0) {
|
||||||
|
this.showMessage(AbstractView.SecurityLevel.SECURE, AbstractView.LINK_SECURE_TITLE, AbstractView.LINK_SECURE_DESCRIPTION, position);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var div = document.createElement('div');
|
||||||
|
div.textContent = AbstractView.LINK_CRITICAL_DESCRIPTION[9999];
|
||||||
|
var ul = document.createElement('ul');
|
||||||
|
div.appendChild(ul);
|
||||||
|
|
||||||
|
while (maliciousLinkType.length > 0) {
|
||||||
|
var li = document.createElement('li');
|
||||||
|
li.innerHTML = AbstractView.LINK_CRITICAL_DESCRIPTION[maliciousLinkType.pop()];
|
||||||
|
ul.appendChild(li);
|
||||||
|
}
|
||||||
|
this.showMessage(AbstractView.SecurityLevel.CRITICAL, AbstractView.LINK_CRITICAL_TITLE, div.outerHTML, position);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Draw indicator near attachments
|
||||||
|
*
|
||||||
|
* @param {HTMLElement} position character position to inject indicator into
|
||||||
|
* @param {boolean} isSuspiciousFile whether or not link is suspicious
|
||||||
|
*/
|
||||||
|
public showSuspiciousFile(position: HTMLElement, isSuspiciousFile: boolean): void {
|
||||||
|
if (isSuspiciousFile) {
|
||||||
|
this.showMessage(AbstractView.SecurityLevel.CRITICAL, AbstractView.FILE_SECURE_TITLE, AbstractView.FILE_SECURE_DESCRIPTION, position);
|
||||||
|
} else {
|
||||||
|
this.showMessage(AbstractView.SecurityLevel.WARNING, AbstractView.FILE_WARNING_TITLE, AbstractView.FILE_WARNING_DESCRIPTION, position);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return indicator for spam word, as string
|
||||||
|
*
|
||||||
|
* @param {string} triggerWord a spam word
|
||||||
|
* @return {string} indicator as string
|
||||||
|
*/
|
||||||
|
public showSpamWord(triggerWord: string): string {
|
||||||
|
return this.showMessage(AbstractView.SecurityLevel.CRITICAL, AbstractView.KEYWORD_CRITICAL_TITLE, AbstractView.KEYWORD_CRITICAL_DESCRIPTION(triggerWord.trim()));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove every indicator on page
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
public clear(): void {
|
||||||
|
var hamspam = document.getElementsByClassName(AbstractView.CLASS);
|
||||||
|
if (hamspam.length > 0) {
|
||||||
|
for (let i = 0; i < hamspam.length; i) {
|
||||||
|
hamspam[0].parentNode.removeChild(hamspam[0]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return svg indicator image
|
||||||
|
*
|
||||||
|
* @param {AbstractView.SecurityLevel} level security level
|
||||||
|
* @return {string} svg indicator image
|
||||||
|
*/
|
||||||
|
public getIndicator(level: AbstractView.SecurityLevel): string {
|
||||||
|
var enums = {
|
||||||
|
[AbstractView.SecurityLevel.SECURE]: AbstractView.INDICATOR_SECURE,
|
||||||
|
[AbstractView.SecurityLevel.WARNING]: AbstractView.INDICATOR_WARNING,
|
||||||
|
[AbstractView.SecurityLevel.CRITICAL]: AbstractView.INDICATOR_CRITICAL
|
||||||
|
}
|
||||||
|
return enums[level];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return css class associated with security level for styling indicator
|
||||||
|
*
|
||||||
|
* @param {AbstractView.SecurityLevel} level security level
|
||||||
|
* @return {string} css class that can be added to and style indicator
|
||||||
|
*/
|
||||||
|
public getIndicatorClass(level: AbstractView.SecurityLevel): string {
|
||||||
|
var enums = {
|
||||||
|
[AbstractView.SecurityLevel.SECURE]: 'hamspam-secure',
|
||||||
|
[AbstractView.SecurityLevel.WARNING]: 'hamspam-warning',
|
||||||
|
[AbstractView.SecurityLevel.CRITICAL]: 'hamspam-critical'
|
||||||
|
}
|
||||||
|
return enums[level];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SecurityLevel - enum class for security level
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
export namespace AbstractView {
|
||||||
|
export enum SecurityLevel {
|
||||||
|
SECURE, WARNING, CRITICAL
|
||||||
|
}
|
||||||
|
}
|
58
src/Config.ts
Normal file
58
src/Config.ts
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
import * as jsyaml from 'js-yaml';
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Config - retrieve configuration settings from config.yaml
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
export class Config {
|
||||||
|
private static instance: Config = null;
|
||||||
|
private doc: object;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor for objects of Config
|
||||||
|
* @constructor
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
private constructor() { }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return instance of Config class
|
||||||
|
*
|
||||||
|
* @return {Config} instance of Config class; if instance is undefined, initialize Config class and return
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
public static getInstance(): Config {
|
||||||
|
if (this.instance === null) {
|
||||||
|
this.instance = new Config();
|
||||||
|
this.instance.readConfigurationFile();
|
||||||
|
}
|
||||||
|
return this.instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load Configuration File
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
private readConfigurationFile(): void {
|
||||||
|
var request = new XMLHttpRequest();
|
||||||
|
request.withCredentials = true;
|
||||||
|
request.open('GET', chrome.runtime.getURL('/config.yaml'), false);
|
||||||
|
request.onload = function() {
|
||||||
|
Config.getInstance().doc = jsyaml.safeLoad(request.responseText);
|
||||||
|
}
|
||||||
|
request.send();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return configuration settings for certain attribute
|
||||||
|
*
|
||||||
|
* @param {string} attr configuration attribute
|
||||||
|
* @return {any} configuration settings associated with the attribute
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
public getConfigurationOf(attr: string): any {
|
||||||
|
return this.doc[attr];
|
||||||
|
}
|
||||||
|
}
|
50
src/Email.ts
Normal file
50
src/Email.ts
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
import { AbstractEmail } from "./AbstractEmail";
|
||||||
|
|
||||||
|
export class Email extends AbstractEmail {
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
public getBody(): HTMLElement {
|
||||||
|
throw new Error('Not Implemented');
|
||||||
|
}
|
||||||
|
|
||||||
|
public getSender(): HTMLElement {
|
||||||
|
throw new Error('Not Implemented');
|
||||||
|
}
|
||||||
|
|
||||||
|
public getSenderString(): string {
|
||||||
|
throw new Error('Not Implemented');
|
||||||
|
}
|
||||||
|
|
||||||
|
public getDeliveryTime(): HTMLElement {
|
||||||
|
throw new Error('Not Implemented');
|
||||||
|
}
|
||||||
|
|
||||||
|
public getDeliveryTimeString(): string {
|
||||||
|
throw new Error('Not Implemented');
|
||||||
|
}
|
||||||
|
|
||||||
|
public getLink(): Array<HTMLAnchorElement> {
|
||||||
|
throw new Error('Not Implemented');
|
||||||
|
}
|
||||||
|
|
||||||
|
public getLinkString(): Array<string> {
|
||||||
|
throw new Error('Not Implemented');
|
||||||
|
}
|
||||||
|
|
||||||
|
public getAttachment(): Array<HTMLElement> {
|
||||||
|
throw new Error('Not Implemented');
|
||||||
|
}
|
||||||
|
|
||||||
|
public getAttachmentString(): Array<string> {
|
||||||
|
throw new Error('Not Implemented');
|
||||||
|
}
|
||||||
|
|
||||||
|
public write(parent: Node, self: Node): void {
|
||||||
|
throw new Error('Not Implemented');
|
||||||
|
}
|
||||||
|
}
|
30
src/EmailView.ts
Normal file
30
src/EmailView.ts
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
import { AbstractView } from "./AbstractView";
|
||||||
|
|
||||||
|
export class EmailView extends AbstractView {
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
public showIsTrustedSender(): boolean {
|
||||||
|
throw new Error('Not Implemented');
|
||||||
|
}
|
||||||
|
|
||||||
|
public showDeliveredLateNight(): boolean {
|
||||||
|
throw new Error('Not Implemented');
|
||||||
|
}
|
||||||
|
|
||||||
|
public showIsMaliciousLink(): boolean {
|
||||||
|
throw new Error('Not Implemented');
|
||||||
|
}
|
||||||
|
|
||||||
|
public showIsSuspiciousFile(): boolean {
|
||||||
|
throw new Error('Not Implemented');
|
||||||
|
}
|
||||||
|
|
||||||
|
public showFindTriggerWord(): boolean {
|
||||||
|
throw new Error('Not Implemented');
|
||||||
|
}
|
||||||
|
}
|
103
src/Hamspam.ts
Normal file
103
src/Hamspam.ts
Normal file
|
@ -0,0 +1,103 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
import { Config } from './Config';
|
||||||
|
import * as Lib from './Lib';
|
||||||
|
import { AbstractEmail } from './AbstractEmail';
|
||||||
|
import { AbstractView } from './AbstractView';
|
||||||
|
import * as $ from 'jquery';
|
||||||
|
|
||||||
|
// Import index.css
|
||||||
|
$(`<link rel="stylesheet" href="${chrome.runtime.getURL('/css/index.css')}">`).appendTo('head');
|
||||||
|
// Import popper.js and bootstrap
|
||||||
|
$('<script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.4.1/js/bootstrap.bundle.min.js"></script>').appendTo('head');
|
||||||
|
|
||||||
|
// Create dynamically email object and view object based on the current page's url
|
||||||
|
for (let conf of Config.getInstance().getConfigurationOf('email')) {
|
||||||
|
var anchor = document.createElement('a');
|
||||||
|
anchor.href = window.location.origin;
|
||||||
|
if (anchor.hostname === conf.hostname) {
|
||||||
|
var email: AbstractEmail = new Lib[conf.name]();
|
||||||
|
var view: AbstractView = new Lib[conf.view]();
|
||||||
|
// Remove all old indicators before drawing new
|
||||||
|
view.clear();
|
||||||
|
|
||||||
|
checkSender();
|
||||||
|
checkDeliveryTime();
|
||||||
|
checkLinks();
|
||||||
|
checkAttachments();
|
||||||
|
checkSpamWord();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkSender(): void {
|
||||||
|
var senderElement = email.getSender();
|
||||||
|
var sender = email.getSenderString();
|
||||||
|
var isTrustedSender = email.isTrustedSender(sender);
|
||||||
|
view.showIfIsTrustedSender(senderElement, isTrustedSender);
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkDeliveryTime(): void {
|
||||||
|
var deliveryTimeElement = email.getDeliveryTime();
|
||||||
|
var deliveryTime = email.getDeliveryTimeString();
|
||||||
|
var deliveredLateNight = email.deliveredLateNight(deliveryTime);
|
||||||
|
view.showIfDeliveredLateNight(deliveryTimeElement, deliveredLateNight);
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkLinks(): void {
|
||||||
|
var linkElements = email.getLink();
|
||||||
|
var links = email.getLinkString();
|
||||||
|
for (let i = 0; i < links.length; i++) {
|
||||||
|
var isMaliciousLink = email.isMaliciousLink(linkElements[i].href);
|
||||||
|
view.showMaliciousLink(linkElements[i], isMaliciousLink);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkAttachments(): void {
|
||||||
|
var attachmentElements = email.getAttachment();
|
||||||
|
var attachments = email.getAttachmentString();
|
||||||
|
for (let i = 0; i < attachments.length; i++) {
|
||||||
|
var isSuspiciousFile = email.isSuspiciousFile(attachments[i]);
|
||||||
|
view.showSuspiciousFile(attachmentElements[i], isSuspiciousFile);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkSpamWord(): void {
|
||||||
|
var spamWords = email.findSpamWords(email.getBody());
|
||||||
|
if (spamWords.length > 0) {
|
||||||
|
var offset = 0;
|
||||||
|
for (let n = 0; n < spamWords.length; n++) {
|
||||||
|
var indicatorInnerHTML = view.showSpamWord(spamWords[n][1]);
|
||||||
|
// Insert indicator
|
||||||
|
email.getBody().innerHTML = email.getBody().innerHTML.substr(0, spamWords[n][0] + offset) + indicatorInnerHTML + email.getBody().innerHTML.substr(spamWords[n][0] + offset);
|
||||||
|
// After each indicator is inserted, adds number of characters of inserted indicator to the position of remaining indicators
|
||||||
|
offset += indicatorInnerHTML.length;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find all images in email
|
||||||
|
var imageSource: Array<string> = new Array<string>();
|
||||||
|
for (let image of email.getBody().getElementsByTagName('img') as any) {
|
||||||
|
// Select only large enough images
|
||||||
|
if (image.width > 100 && image.height > 40) {
|
||||||
|
imageSource.push(image.src);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Recognize and tests text in images
|
||||||
|
email.imageToString(imageSource).then((value: object) => {
|
||||||
|
// For text in each image
|
||||||
|
for (let imageIdx in Object.values(value)) {
|
||||||
|
// Create an HTML element with the text in image as innerHTML
|
||||||
|
var span: HTMLElement = document.createElement('span');
|
||||||
|
span.innerHTML = Object.values(value)[imageIdx].data.text;
|
||||||
|
// Pass the created HTML element to findSpamWords function
|
||||||
|
var spamWords = email.findSpamWords(span);
|
||||||
|
// For each spam word found within the HTML element
|
||||||
|
for (let spamWordIdx in spamWords) {
|
||||||
|
// Insert indicator before the associaited image
|
||||||
|
var span: HTMLElement = document.createElement('span');
|
||||||
|
span.innerHTML = view.showSpamWord(spamWords[spamWordIdx][1]);
|
||||||
|
email.getBody().querySelectorAll('img')[imageIdx].before(span);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
5
src/Lib.ts
Normal file
5
src/Lib.ts
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
export * from './Outlook';
|
||||||
|
export * from './Email';
|
||||||
|
|
||||||
|
export * from './OutlookView';
|
||||||
|
export * from './EmailView';
|
105
src/Outlook.ts
Normal file
105
src/Outlook.ts
Normal file
|
@ -0,0 +1,105 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
import { AbstractEmail } from "./AbstractEmail";
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Outlook - AbstractEmail childclass for Microsoft Outlook
|
||||||
|
*
|
||||||
|
* @version 1.0 2019-10
|
||||||
|
*/
|
||||||
|
export class Outlook extends AbstractEmail {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor for objects of Outlook
|
||||||
|
* @constructor
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return container of email body
|
||||||
|
*
|
||||||
|
* @return {HTMLElement} container of email body
|
||||||
|
*/
|
||||||
|
public getBody(): HTMLElement {
|
||||||
|
return document.querySelector("._3U2q6dcdZCrTrR_42Nxby.JWNdg1hee9_Rz6bIGvG1c");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return container of sender email address
|
||||||
|
*
|
||||||
|
* @return {HTMLElement} container of sender email addresss
|
||||||
|
*/
|
||||||
|
public getSender(): HTMLElement {
|
||||||
|
return document.querySelector("div._3FAYod7kjH9o5HZX_Phvf6 > div._1Lo7BjmdsKZy3IMMxN7mVu > div:nth-child(1)");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return string of sender email address
|
||||||
|
*
|
||||||
|
* @return {string} sender email addresss
|
||||||
|
*/
|
||||||
|
public getSenderString(): string {
|
||||||
|
return document.querySelector('div._5CGGutaz4d1vhT3GzbRJq > div > div:nth-child(2) > div > div > span').getAttribute('title');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return container of delivery time
|
||||||
|
*
|
||||||
|
* @return {HTMLElement} container of delivery time
|
||||||
|
*/
|
||||||
|
public getDeliveryTime(): HTMLElement {
|
||||||
|
return document.querySelector("div._3FAYod7kjH9o5HZX_Phvf6 > div._1Lo7BjmdsKZy3IMMxN7mVu > div:nth-child(2)");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return string of delivery time
|
||||||
|
*
|
||||||
|
* @return {string} delivery time
|
||||||
|
*/
|
||||||
|
public getDeliveryTimeString(): string {
|
||||||
|
return this.getDeliveryTime().textContent;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return an array of all links in email
|
||||||
|
*
|
||||||
|
* @return {Array<HTMLAnchorElement>} array of anchor tags
|
||||||
|
*/
|
||||||
|
public getLink(): Array<HTMLAnchorElement> {
|
||||||
|
var elements = this.getBody().parentElement.querySelectorAll('a');
|
||||||
|
return Array.prototype.map.call(elements, function(element: HTMLAnchorElement) { return element; });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return an array of all link text in email
|
||||||
|
*
|
||||||
|
* @return {Array<string>} array of link text
|
||||||
|
*/
|
||||||
|
public getLinkString(): Array<string> {
|
||||||
|
var elements = this.getBody().parentElement.querySelectorAll('a');
|
||||||
|
return Array.prototype.map.call(elements, function(element: HTMLAnchorElement) { return element.textContent; });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return an array of containers of all attachments
|
||||||
|
*
|
||||||
|
* @return {Array<HTMLElement>} array of containers of attachments
|
||||||
|
*/
|
||||||
|
public getAttachment(): Array<HTMLElement> {
|
||||||
|
var elements = document.querySelectorAll('div.jgenqigMC4s0jMUDuG-YY > div > div > div');
|
||||||
|
return Array.prototype.map.call(elements, function(element: Element) { return element; });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return an array of attachment file names
|
||||||
|
*
|
||||||
|
* @return {Array<string>} array of attachment file names
|
||||||
|
*/
|
||||||
|
public getAttachmentString(): Array<string> {
|
||||||
|
var elements = document.querySelectorAll('div.jgenqigMC4s0jMUDuG-YY > div > div > div');
|
||||||
|
return Array.prototype.map.call(elements, function(element: Element) { return element.textContent; });
|
||||||
|
}
|
||||||
|
}
|
37
src/OutlookView.ts
Normal file
37
src/OutlookView.ts
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
import { AbstractView } from './AbstractView';
|
||||||
|
import { Outlook } from './Outlook';
|
||||||
|
|
||||||
|
/*
|
||||||
|
* OutlookView - AbstractView childclass for Microsoft Outlook
|
||||||
|
*
|
||||||
|
* @version 1.0 2019-10
|
||||||
|
*/
|
||||||
|
export class OutlookView extends AbstractView {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor for objects of OutlookView
|
||||||
|
* @constructor
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
// Fix popover to the associated button
|
||||||
|
new Outlook().getBody().style.position = 'relative';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Draw indicator near attachments
|
||||||
|
*
|
||||||
|
* @param {HTMLElement} position character position to inject indicator into
|
||||||
|
* @param {boolean} isSuspiciousFile whether or not link is suspicious
|
||||||
|
*/
|
||||||
|
public showSuspiciousFile(position: HTMLElement, isSuspiciousFile: boolean): void {
|
||||||
|
// Expand attachment pane
|
||||||
|
var attachmentPane : HTMLElement = document.querySelector('div.jgenqigMC4s0jMUDuG-YY > div > div');
|
||||||
|
attachmentPane.style.height = '100%';
|
||||||
|
|
||||||
|
super.showSuspiciousFile(position, isSuspiciousFile);
|
||||||
|
}
|
||||||
|
}
|
15
tsconfig.json
Normal file
15
tsconfig.json
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"outDir": "./dist/",
|
||||||
|
"sourceMap": true,
|
||||||
|
"noImplicitAny": false,
|
||||||
|
"module": "es6",
|
||||||
|
"target": "es5",
|
||||||
|
"allowJs": true,
|
||||||
|
"watch": true,
|
||||||
|
},
|
||||||
|
"exclude": [
|
||||||
|
"test.ts",
|
||||||
|
"node_modules",
|
||||||
|
]
|
||||||
|
}
|
22
webpack.config.js
Normal file
22
webpack.config.js
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
entry: './src/Hamspam.ts',
|
||||||
|
devtool: 'inline-source-map',
|
||||||
|
module: {
|
||||||
|
rules: [
|
||||||
|
{
|
||||||
|
test: /\.tsx?$/,
|
||||||
|
use: 'ts-loader',
|
||||||
|
exclude: /node_modules/,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
resolve: {
|
||||||
|
extensions: ['.js', '.ts', '.tsx'],
|
||||||
|
},
|
||||||
|
output: {
|
||||||
|
filename: 'js/bundle.js',
|
||||||
|
path: path.resolve(__dirname, 'dist'),
|
||||||
|
},
|
||||||
|
};
|
Loading…
Reference in a new issue