This commit is contained in:
7ae 2022-11-12 04:03:00 -08:00
commit f0f6f0c393
No known key found for this signature in database
GPG key ID: BC3C0FF6A278DD5D
25 changed files with 41097 additions and 0 deletions

21
LICENSE Normal file
View 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
View 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&nbsp;&nbsp;up (2)

8
dist/background.js vendored Normal file
View 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
View 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
View 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

File diff suppressed because one or more lines are too long

26
dist/manifest.json vendored Normal file
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 687 B

BIN
docs/assets/img/pass.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 615 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

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

File diff suppressed because it is too large Load diff

27
package.json Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View file

@ -0,0 +1,5 @@
export * from './Outlook';
export * from './Email';
export * from './OutlookView';
export * from './EmailView';

105
src/Outlook.ts Normal file
View 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
View 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
View 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
View 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'),
},
};