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