├── .babelrc ├── .gitignore ├── example ├── example.jpg ├── index.js ├── style.css ├── index.html └── bundle.js ├── .npmignore ├── .eslintrc ├── .travis.yml ├── src ├── ratio.js ├── simpleText.js ├── text.js ├── emailObfuscate.js ├── pseudoElement.js └── canvasText.js ├── test ├── ratio.js ├── simpleText.js ├── emailObfuscate.js ├── pseduoElement.js └── canvasText.js ├── karma.conf.js ├── package.json ├── webpack.config.js ├── readme.md └── lib └── emailObfuscate.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015"] 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | -------------------------------------------------------------------------------- /example/example.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dunckr/email-obfuscate/HEAD/example/example.jpg -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | .git 4 | test 5 | example 6 | karma* 7 | .eslintrc 8 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "airbnb/base", 3 | "rules": { 4 | "semi": [2, "never"] 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "4.2.4" 4 | cache: 5 | directories: 6 | - node_modules 7 | -------------------------------------------------------------------------------- /src/ratio.js: -------------------------------------------------------------------------------- 1 | export default class Ratio { 2 | 3 | constructor(context) { 4 | this.context = context 5 | } 6 | 7 | calculate() { 8 | return this._devicePixelRatio() / this._backingStorePixelRatio() 9 | } 10 | 11 | _devicePixelRatio() { 12 | return window.devicePixelRatio || 1 13 | } 14 | 15 | _backingStorePixelRatio() { 16 | return this.context.webkitBackingStorePixelRatio || 17 | this.context.mozBackingStorePixelRatio || 18 | this.context.msBackingStorePixelRatio || 19 | this.context.oBackingStorePixelRatio || 20 | this.context.backingStorePixelRatio || 1 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/simpleText.js: -------------------------------------------------------------------------------- 1 | import BaseText from './text' 2 | 3 | export default class SimpleText extends BaseText { 4 | 5 | _createElement() { 6 | const exisitingElement = this.parent.getElementsByTagName('a').length > 0 7 | if (exisitingElement) { 8 | this.element = this.parent.getElementsByTagName('a')[0] 9 | } else { 10 | this.element = document.createElement('a') 11 | } 12 | this.element.innerHTML = this.options.altText 13 | this._styleElement() 14 | if (!exisitingElement) { 15 | this.parent.appendChild(this.element) 16 | } 17 | } 18 | 19 | _styleElement() { 20 | this.element.style.cursor = 'pointer' 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /src/text.js: -------------------------------------------------------------------------------- 1 | export default class Text { 2 | 3 | constructor(parent, options) { 4 | if (!parent) { throw new Error('Require DOM element') } 5 | if (!options) { throw new Error('Require options') } 6 | this.parent = parent 7 | this.options = options 8 | } 9 | 10 | create() { 11 | this._createElement() 12 | this._addEvents() 13 | return this.element 14 | } 15 | 16 | handleOnClick() { 17 | window.location.href = `mailto:${this.options.text}` 18 | } 19 | 20 | _addEvents() { 21 | if (this.parent.addEventListener) { 22 | this.parent.addEventListener('click', () => this.handleOnClick()) 23 | } else if (this.parent.attachEvent) { 24 | this.parent.attachEvent('onclick', () => this.handleOnClick()) 25 | } 26 | } 27 | 28 | _createElement() {} 29 | 30 | } 31 | -------------------------------------------------------------------------------- /test/ratio.js: -------------------------------------------------------------------------------- 1 | import expect from 'expect' 2 | import describeClass from '../src/ratio' 3 | 4 | describe('Ratio', () => { 5 | let subject 6 | const context = (ratio) => { 7 | return { 8 | webkitBackingStorePixelRatio: ratio, 9 | mozBackingStorePixelRatio: ratio, 10 | msBackingStorePixelRatio: ratio, 11 | oBackingStorePixelRatio: ratio, 12 | backingStorePixelRatio: ratio 13 | } 14 | } 15 | 16 | it('should determine the correct pixel ratio', () => { 17 | const values = [{ 18 | input: 1, 19 | output: 1 20 | }, { 21 | input: 2, 22 | output: 0.5 23 | }, { 24 | input: 4, 25 | output: 0.25 26 | }] 27 | 28 | values.map((v) => { 29 | subject = new describeClass(context(v.input)) 30 | expect(subject.calculate()).toEqual(v.output) 31 | }) 32 | }) 33 | 34 | }) 35 | -------------------------------------------------------------------------------- /test/simpleText.js: -------------------------------------------------------------------------------- 1 | import expect from 'expect' 2 | import describeClass from '../src/simpleText' 3 | 4 | describe('SimpleText', () => { 5 | let subject 6 | let options = {} 7 | let el 8 | 9 | beforeEach(() => { 10 | el = document.createElement('div') 11 | document.body.appendChild(el) 12 | }) 13 | 14 | afterEach(() => { 15 | document.body.removeChild(el) 16 | }) 17 | 18 | it('should throw an error without options', () => { 19 | expect(() => new describeClass(el)).toThrow() 20 | }) 21 | 22 | beforeEach(() => { 23 | options = { 24 | altText: 'Alternate' 25 | } 26 | subject = new describeClass(el, options) 27 | }) 28 | 29 | describe('creates an element', () => { 30 | 31 | it('should have a text set to altText', () => { 32 | expect(subject.create().innerText).toEqual(options.altText) 33 | }) 34 | 35 | }) 36 | 37 | }) 38 | -------------------------------------------------------------------------------- /test/emailObfuscate.js: -------------------------------------------------------------------------------- 1 | import expect from 'expect' 2 | import describedSpec from '../src/emailObfuscate' 3 | 4 | describe('EmailObfuscate', () => { 5 | let subject 6 | let el 7 | let canvas 8 | 9 | beforeEach(() => { 10 | el = document.createElement('div') 11 | document.body.appendChild(el) 12 | subject = describedSpec(el) 13 | canvas = el.getElementsByTagName('canvas')[0] 14 | }) 15 | 16 | afterEach(() => { 17 | document.body.removeChild(el) 18 | }) 19 | 20 | describe('creates a canvas element', () => { 21 | 22 | it('should have a canvas', () => { 23 | expect(typeof canvas).toBe('object') 24 | }) 25 | 26 | it('should have specific width', () => { 27 | expect(canvas.width).toBeGreaterThan(110) 28 | }) 29 | 30 | it('should have specific height', () => { 31 | expect(canvas.height).toBeGreaterThan(15) 32 | }) 33 | 34 | }) 35 | }) 36 | -------------------------------------------------------------------------------- /src/emailObfuscate.js: -------------------------------------------------------------------------------- 1 | import assign from 'object-assign' 2 | import PseudoElement from './pseudoElement' 3 | import CanvasText from './canvasText' 4 | import SimpleText from './simpleText' 5 | 6 | const DEFAULTS = { 7 | name: 'email', 8 | domain: 'obfuscate', 9 | tld: 'js', 10 | altText: 'Email', 11 | } 12 | 13 | export class EmailObfuscate { 14 | 15 | constructor(el, opts = {}) { 16 | this.el = el 17 | const options = assign(DEFAULTS, opts) 18 | if (window.HTMLCanvasElement) return this._pseudoElement(options) 19 | return this._simpleText(options) 20 | } 21 | 22 | // private 23 | 24 | _pseudoElement(options) { 25 | const pseudoElement = new PseudoElement(this.el, options) 26 | const style = pseudoElement.determineStyle() 27 | const canvasText = new CanvasText(this.el, style) 28 | return canvasText.create() 29 | } 30 | 31 | _simpleText(options) { 32 | const simpleText = new SimpleText(this.el, options) 33 | return simpleText.create() 34 | } 35 | } 36 | 37 | export default (el, opts) => { 38 | return new EmailObfuscate(el, opts) 39 | } 40 | -------------------------------------------------------------------------------- /test/pseduoElement.js: -------------------------------------------------------------------------------- 1 | import expect from 'expect' 2 | import describeClass from '../src/pseudoElement' 3 | 4 | describe('PseudoElement', () => { 5 | let subject 6 | let el 7 | let canvas 8 | 9 | beforeEach(() => { 10 | el = document.createElement('div') 11 | document.body.appendChild(el) 12 | }) 13 | 14 | afterEach(() => { 15 | document.body.removeChild(el) 16 | }) 17 | 18 | it('should throw an error without element', () => { 19 | expect(() => new describeClass()).toThrow() 20 | }) 21 | 22 | it('should determine attributes of the psedueo element', () => { 23 | const options = { 24 | name: 'email', 25 | domain: 'obfuscate', 26 | tld: 'js' 27 | } 28 | subject = new describeClass(el, options) 29 | const style = subject.determineStyle() 30 | expect(style.color).toEqual('rgb(0, 0, 0)') 31 | expect(style.fontSize).toBe(16) 32 | expect(style.font).toContain('16px') 33 | expect(style.text).toEqual('email@obfuscate.js') 34 | expect(style.underline).toEqual(false) 35 | expect(style.width).toBeGreaterThan(110) 36 | expect(style.height).toBeGreaterThan(15) 37 | }) 38 | 39 | }) 40 | -------------------------------------------------------------------------------- /test/canvasText.js: -------------------------------------------------------------------------------- 1 | import expect from 'expect' 2 | import describeClass from '../src/canvasText' 3 | 4 | describe('CanvasText', () => { 5 | let subject 6 | let options = {} 7 | let el 8 | 9 | beforeEach(() => { 10 | el = document.createElement('div') 11 | document.body.appendChild(el) 12 | }) 13 | 14 | afterEach(() => { 15 | document.body.removeChild(el) 16 | }) 17 | 18 | it('should throw an error without options', () => { 19 | expect(() => new describeClass(el)).toThrow() 20 | }) 21 | 22 | beforeEach(() => { 23 | options = { 24 | color: 'rgb(0, 0, 0)', 25 | font: '', 26 | fontSize: 16, 27 | height: 17, 28 | text: 'email@obfuscate.js', 29 | underline: false, 30 | width: 123 31 | } 32 | subject = new describeClass(el, options) 33 | }) 34 | 35 | describe('creates a canvas element', () => { 36 | 37 | it('should have a set width', () => { 38 | expect(subject.create().width).toEqual(options.width) 39 | }) 40 | 41 | it('should have a set height', () => { 42 | expect(subject.create().height).toEqual(options.height) 43 | }) 44 | 45 | }) 46 | 47 | }) 48 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const webpack = require('webpack') 3 | 4 | const paths = { 5 | SRC: path.resolve(__dirname, 'src'), 6 | TEST: path.resolve(__dirname, 'test') 7 | } 8 | 9 | module.exports = (config) => { 10 | config.set({ 11 | 12 | frameworks: ['mocha'], 13 | 14 | preprocessors: { 15 | 'test/**/*': ['webpack'], 16 | }, 17 | 18 | reporters: ['dots'], 19 | 20 | files: [ 21 | 'test/**/*' 22 | ], 23 | 24 | colors: true, 25 | 26 | logLevel: config.LOG_ERROR, 27 | 28 | autoWatch: true, 29 | 30 | webpack: { 31 | module: { 32 | loaders: [{ 33 | test: /\.js?$/, 34 | loader: 'babel-loader', 35 | include: [paths.SRC, paths.TEST], 36 | exclude: /node_modules/ 37 | }], 38 | preLoaders: [{ 39 | test: /\.js?$/, 40 | loader: 'eslint', 41 | include: paths.SRC, 42 | exclude: /node_modules/ 43 | }] 44 | }, 45 | plugins: [ 46 | new webpack.DefinePlugin({ 47 | 'process.env.NODE_ENV': JSON.stringify('test') 48 | }) 49 | ] 50 | }, 51 | 52 | browsers: ['PhantomJS'], 53 | 54 | webpackMiddleware: { 55 | noInfo: true 56 | }, 57 | 58 | singleRun: false 59 | 60 | }) 61 | } 62 | -------------------------------------------------------------------------------- /src/pseudoElement.js: -------------------------------------------------------------------------------- 1 | export default class PseudoElement { 2 | 3 | constructor(parent, options) { 4 | if (!parent) { throw new Error('Require DOM element') } 5 | this.parent = parent 6 | this.options = options 7 | } 8 | 9 | determineStyle() { 10 | this._createElement() 11 | this._insertElement() 12 | const computedStyle = this._computeStyle() 13 | const fontSizeNumber = Number(computedStyle.fontSize.slice(0, -2)) 14 | const attributes = { 15 | font: `${computedStyle.fontSize} ${computedStyle.fontFamily}`, 16 | fontSize: fontSizeNumber, 17 | color: computedStyle.color, 18 | width: this.element.offsetWidth, 19 | height: this.element.offsetHeight, 20 | underline: computedStyle.textDecoration === 'underline', 21 | text: this._generateText(), 22 | } 23 | this.parent.removeChild(this.element) 24 | return attributes 25 | } 26 | 27 | _createElement() { 28 | this.element = document.createElement('a') 29 | this.element.style.visibility = 'hidden' 30 | this.element.innerHTML = this._obfuscateText() 31 | } 32 | 33 | _insertElement() { 34 | this.parent.appendChild(this.element) 35 | } 36 | 37 | _computeStyle() { 38 | return window.getComputedStyle(this.element) 39 | } 40 | 41 | _obfuscateText() { 42 | return `@.${this.options.tld}${this.options.domain}${this.options.name}` 43 | } 44 | 45 | _generateText() { 46 | return `${this.options.name}@${this.options.domain}.${this.options.tld}` 47 | } 48 | 49 | } 50 | -------------------------------------------------------------------------------- /src/canvasText.js: -------------------------------------------------------------------------------- 1 | import BaseText from './text' 2 | import Ratio from './ratio' 3 | 4 | export default class CanvasText extends BaseText { 5 | 6 | _createElement() { 7 | this._createCanvas() 8 | this._createText() 9 | } 10 | 11 | _createCanvas() { 12 | const exisitingCanvas = this.parent.getElementsByTagName('canvas').length > 0 13 | if (exisitingCanvas) { 14 | this.element = this.parent.getElementsByTagName('canvas')[0] 15 | } else { 16 | this.element = document.createElement('canvas') 17 | } 18 | this.context = this.element.getContext('2d') 19 | this._styleCanvas() 20 | if (!exisitingCanvas) { 21 | this.parent.appendChild(this.element) 22 | } 23 | } 24 | 25 | _styleCanvas() { 26 | const width = this.options.width 27 | const height = this.options.height 28 | const ratio = new Ratio(this.context).calculate() 29 | this.element.width = width * ratio 30 | this.element.height = height * ratio 31 | this.element.style.width = `${width}px` 32 | this.element.style.height = `${height}px` 33 | this.context.scale(ratio, ratio) 34 | this.element.style.cursor = 'pointer' 35 | } 36 | 37 | _createText() { 38 | this.context.textAlign = 'left' 39 | this.context.textBaseline = 'bottom' 40 | this.context.font = this.options.font 41 | this.context.fillStyle = this.options.color 42 | this.context.fillText(this.options.text, 0, this.options.height) 43 | if (this.options.underline) { 44 | const underlineSize = this.options.fontSize / 10 45 | const offset = underlineSize 46 | const y = this.options.height - underlineSize - offset 47 | this.context.fillRect(0, y, this.options.width, underlineSize) 48 | } 49 | } 50 | 51 | } 52 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "email-obfuscate", 3 | "version": "0.0.6", 4 | "description": "Guard email addresses from being simply scraped by bots.", 5 | "main": "lib/emailObfuscate.js", 6 | "scripts": { 7 | "start": "npm run clean && webpack-dev-server --hot", 8 | "test": "karma start --single-run", 9 | "karma": "karma start", 10 | "lint": "eslint src", 11 | "build": "npm run clean && npm run lint && karma start --single-run && npm run build-example && npm run build-lib", 12 | "build-lib": "webpack", 13 | "build-example": "webpack", 14 | "clean": "rm -f example/bundle.js example/bundle.js.map && rm -rf lib && mkdir lib", 15 | "gh-pages": "git checkout gh-pages && git rebase origin/master --force-rebase && npm run build && git add . && git commit --amend --no-edit && git push --force && git checkout master" 16 | }, 17 | "repository": { 18 | "type": "git", 19 | "url": "git@github.com:dunckr/email-obfuscate" 20 | }, 21 | "keywords": [ 22 | "email", 23 | "obfuscate", 24 | "guard", 25 | "scrape", 26 | "bot", 27 | "prevent", 28 | "stop" 29 | ], 30 | "author": "Duncan Beaton ", 31 | "license": "MIT", 32 | "bugs": { 33 | "url": "https://github.com/dunckr/email-obfuscate/issues" 34 | }, 35 | "homepage": "https://github.com/dunckr/email-obfuscate", 36 | "dependencies": { 37 | "object-assign": "^4.0.1" 38 | }, 39 | "devDependencies": { 40 | "babel-core": "^6.4.0", 41 | "babel-eslint": "^5.0.0-beta6", 42 | "babel-loader": "^6.2.1", 43 | "babel-preset-es2015": "^6.3.13", 44 | "eslint": "^1.10.3", 45 | "eslint-config-airbnb": "^3.0.2", 46 | "eslint-loader": "^1.2.0", 47 | "expect": "^1.13.4", 48 | "karma": "^0.13.19", 49 | "karma-cli": "^0.1.2", 50 | "karma-mocha": "^0.2.1", 51 | "karma-phantomjs-launcher": "^0.2.3", 52 | "karma-webpack": "^1.7.0", 53 | "mocha": "^2.3.4", 54 | "phantomjs": "^1.9.19", 55 | "webpack": "^1.12.10", 56 | "webpack-dev-server": "^1.14.0" 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /example/index.js: -------------------------------------------------------------------------------- 1 | import EmailObfuscate from '../src/emailObfuscate' 2 | 3 | var fontList = [ 4 | 'Arial', 5 | 'Courier New', 6 | 'Lucida Bright', 7 | 'Palatino', 8 | 'PT Serif', 9 | 'Trebuchet MS', 10 | 'Times New Roman', 11 | 'Verdana', 12 | ] 13 | 14 | var webFonts = [ 15 | 'Abril Fatface', 16 | 'Arvo', 17 | 'Droid Sans', 18 | 'Fira Sans', 19 | 'Josefin Sans', 20 | 'Lato', 21 | 'Old Standard TT', 22 | 'Open Sans', 23 | 'Roboto', 24 | 'Ubuntu', 25 | 'Volkhov' 26 | ] 27 | 28 | WebFont.load({ 29 | google: { 30 | families: webFonts 31 | }, 32 | active: () => { 33 | fontList = fontList.concat(webFonts) 34 | emailObfuscate() 35 | generateSelection() 36 | } 37 | }) 38 | 39 | var emailObfuscate = () => { 40 | var el = document.getElementById('obfuscated') 41 | EmailObfuscate(el, {}) 42 | } 43 | 44 | var generateStyle = (fontFamily) => { 45 | var css = `.example__a { font-family: "${fontFamily}" }` 46 | var style = document.createElement('style') 47 | style.appendChild(document.createTextNode(css)) 48 | var exisitingStyle = document.head.getElementsByTagName('style') 49 | if (exisitingStyle.length > 0) { 50 | document.head.removeChild(exisitingStyle[0]) 51 | } 52 | document.head.appendChild(style) 53 | } 54 | 55 | var fontSelectionEl = document.getElementById('fontSelection') 56 | 57 | var generateSelection = () => { 58 | while (fontSelectionEl.firstChild) { 59 | fontSelectionEl.removeChild(fontSelectionEl.firstChild) 60 | } 61 | fontList.map((font) => { 62 | var el = document.createElement('option') 63 | el.innerHTML = font 64 | el.value = font 65 | fontSelectionEl.appendChild(el) 66 | }) 67 | } 68 | 69 | var updateSource = (input, output) => { 70 | var htmlStr = document.getElementById(input).outerHTML 71 | var clearHtml = document.getElementById(output) 72 | clearHtml.innerText = htmlStr 73 | } 74 | 75 | var updateSources = () => { 76 | updateSource('clear', 'clearHtml') 77 | updateSource('obfuscated', 'obfuscatedHtml') 78 | } 79 | 80 | fontSelectionEl.addEventListener('change', (e) => { 81 | var fontFamily = e.target.value 82 | generateStyle(fontFamily) 83 | emailObfuscate() 84 | }) 85 | 86 | window.addEventListener('resize', () => { 87 | emailObfuscate() 88 | }) 89 | 90 | emailObfuscate() 91 | generateSelection() 92 | updateSources() 93 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const webpack = require('webpack') 3 | 4 | const TARGET = process.env.npm_lifecycle_event 5 | const config = { 6 | library: 'EmailObfuscate', 7 | filename: 'emailObfuscate', 8 | example: 'index', 9 | bundle: 'bundle' 10 | } 11 | const paths = { 12 | SRC: path.resolve(__dirname, './src'), 13 | EXAMPLE: path.resolve(__dirname, './example'), 14 | BUILD: path.resolve(__dirname, './lib') 15 | } 16 | 17 | var webpackBase = { 18 | output: { 19 | path: paths.EXAMPLE, 20 | filename: `${config.bundle}.js` 21 | }, 22 | module: { 23 | loaders: [{ 24 | test: /\.js?$/, 25 | loader: 'babel-loader', 26 | include: [paths.SRC, paths.EXAMPLE], 27 | exclude: /node_modules/ 28 | }], 29 | preLoaders: [{ 30 | test: /\.js?$/, 31 | loader: 'eslint', 32 | include: paths.SRC, 33 | exclude: /node_modules/ 34 | }] 35 | } 36 | } 37 | 38 | if (TARGET === 'start' || !TARGET) { 39 | module.exports = Object.assign(webpackBase, { 40 | entry: [ 41 | 'webpack/hot/dev-server', 42 | `${paths.EXAMPLE}/${config.example}.js` 43 | ], 44 | devtool: 'source-map', 45 | devServer: { 46 | contentBase: paths.EXAMPLE, 47 | inline: true, 48 | progress: true, 49 | port: '8080' 50 | }, 51 | plugins: [ 52 | new webpack.DefinePlugin({ 53 | 'process.env.NODE_ENV': JSON.stringify('development') 54 | }) 55 | ] 56 | }) 57 | } 58 | 59 | if (TARGET === 'build-example') { 60 | module.exports = Object.assign(webpackBase, { 61 | entry: `${paths.EXAMPLE}/${config.example}.js`, 62 | plugins: [ 63 | new webpack.DefinePlugin({ 64 | 'process.env.NODE_ENV': JSON.stringify('production') 65 | }), 66 | new webpack.optimize.UglifyJsPlugin({ 67 | compress: { 68 | warnings: false 69 | } 70 | }) 71 | ] 72 | }) 73 | } 74 | 75 | if (TARGET === 'build-lib') { 76 | module.exports = Object.assign(webpackBase, { 77 | entry: `${paths.SRC}/${config.filename}.js`, 78 | output: { 79 | path: paths.BUILD, 80 | libraryTarget: 'umd', 81 | library: config.library, 82 | filename: config.filename + '.js' 83 | }, 84 | module: { 85 | loaders: [{ 86 | test: /\.js?$/, 87 | loader: 'babel-loader', 88 | include: [paths.SRC], 89 | exclude: /node_modules/ 90 | }], 91 | }, 92 | plugins: [ 93 | new webpack.DefinePlugin({ 94 | 'process.env.NODE_ENV': JSON.stringify('production') 95 | }), 96 | new webpack.optimize.UglifyJsPlugin({ 97 | compress: { 98 | warnings: false 99 | } 100 | }) 101 | ] 102 | }) 103 | } 104 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Email Obfuscate [![Build Status](https://travis-ci.org/dunckr/email-obfuscate.svg?branch=master)](https://travis-ci.org/dunckr/email-obfuscate) 2 | 3 | ![Example](https://raw.githubusercontent.com/dunckr/email-obfuscate/master/example/example.jpg) 4 | 5 | Guard email addresses from being simply scraped by bots. 6 | 7 | ## Demo 8 | 9 | [http://dunckr.github.io/email-obfuscate/example/](http://dunckr.github.io/email-obfuscate/example/) 10 | 11 | ## Overview 12 | 13 | Email addresses are [harvested](https://en.wikipedia.org/wiki/Email_address_harvesting) from websites using [web scrapers](https://github.com/lorien/awesome-web-scraping). 14 | 15 | There are many strategies that can be employed to mitigate scrapers: 16 | [1,](http://security.stackexchange.com/questions/81964/are-web-scrapers-fooled-by-obscured-emails-anymore) 17 | [2,](https://www.quora.com/Whats-the-best-way-to-prevent-email-scraping) 18 | [3,](http://stackoverflow.com/questions/3161548/how-do-i-prevent-site-scraping) 19 | [4.](http://stackoverflow.com/questions/23002711/how-to-show-email-addresses-on-the-website-to-avoid-spams) 20 | However, it must be noted that preventing scraping is an [arms-race](https://en.wikipedia.org/wiki/Arms_race). 21 | 22 | This library's aim to prevent searching for ```mailto``` links or using email address regexes. 23 | 24 | It does this by constructing an email address from an object then drawing the text as an image using the canvas. 25 | 26 | For older browsers without a canvas (IE8) we append an element using the alternate text. 27 | 28 | ## Installation 29 | 30 | ```sh 31 | npm install email-obfuscate --save 32 | ``` 33 | 34 | ## Usage 35 | 36 | ```js 37 | import EmailObfuscate from 'email-obfuscate'; 38 | 39 | var el = document.getElementById('email'); 40 | 41 | EmailObfuscate(el, { 42 | // Email construct: name@domain.tld 43 | name: 'test', 44 | domain: 'example', 45 | tld: 'com', 46 | // Alternate Text 47 | altText: 'Email' 48 | }); 49 | ``` 50 | 51 | ## API 52 | 53 | ### EmailObfuscate(el, [options]) 54 | 55 | #### el 56 | 57 | Type: `HTMLElement` 58 | 59 | The element to replace with EmailObfuscate. 60 | 61 | #### options 62 | 63 | ##### name 64 | 65 | Type: `string` 66 | Default: `test` 67 | 68 | The name portion of the email address to use (**name**@email.com). 69 | 70 | ##### domain 71 | 72 | Type: `string` 73 | Default: `example` 74 | 75 | The domain name portion of the email address to use (name@**email**.com). 76 | 77 | ##### tld 78 | 79 | Type: `string` 80 | Default: `com` 81 | 82 | The top-level domain portion of the email address to use (name@email.**com**). 83 | 84 | ##### altText 85 | 86 | Type: `string` 87 | Default: `Email` 88 | 89 | The alternate text to use to represent the email address. 90 | 91 | ## Dev 92 | 93 | ```sh 94 | npm run start 95 | ``` 96 | 97 | ## Test 98 | 99 | ```sh 100 | npm run test 101 | ``` 102 | 103 | ## Build 104 | 105 | ```sh 106 | npm run build 107 | ``` 108 | 109 | ## License 110 | 111 | MIT © [Duncan Beaton](http://dunckr.com) 112 | -------------------------------------------------------------------------------- /example/style.css: -------------------------------------------------------------------------------- 1 | html { 2 | background-color: #E1EBF7; 3 | -webkit-font-smoothing: antialiased; 4 | } 5 | 6 | body { 7 | font: 100%/1.5 "Fira Sans", Verdana, sans-serif; 8 | background-color: #FFFFFF; 9 | color: #494949; 10 | } 11 | 12 | .row:after { 13 | content: ""; 14 | display: table; 15 | clear: both; 16 | } 17 | 18 | .columns { 19 | width: 100%; 20 | float: left; 21 | } 22 | 23 | .six.columns { 24 | width: 48%; 25 | } 26 | 27 | @media (min-width: 800px) { 28 | .two.columns { 29 | width: 13.3333333333%; 30 | } 31 | } 32 | 33 | @media (min-width: 800px) { 34 | .ten.columns { 35 | width: 82.6666666667%; 36 | } 37 | } 38 | 39 | .title { 40 | padding: 80px 40px 0; 41 | } 42 | 43 | .title .icon { 44 | text-align: center; 45 | } 46 | 47 | .title h1 { 48 | text-align: center; 49 | text-transform: uppercase; 50 | } 51 | 52 | .title p { 53 | text-align: center; 54 | } 55 | 56 | p, 57 | span { 58 | color: #A5A5A5; 59 | } 60 | 61 | a { 62 | color: #94C8FF; 63 | text-decoration: underline; 64 | } 65 | 66 | section { 67 | padding: 5px; 68 | } 69 | 70 | @media (min-width: 800px) { 71 | section { 72 | padding: 40px; 73 | } 74 | } 75 | 76 | form { 77 | text-align: center; 78 | } 79 | 80 | @media (min-width: 800px) { 81 | .control { 82 | padding-left: 20%; 83 | padding-right: 20%; 84 | } 85 | } 86 | 87 | .control label { 88 | line-height: 2; 89 | } 90 | 91 | .example { 92 | background-color: #F8F9FA; 93 | color: #4D4D4D; 94 | } 95 | 96 | @media (min-width: 800px) { 97 | section { 98 | padding-left: 20%; 99 | padding-right: 20%; 100 | } 101 | } 102 | 103 | .example span { 104 | line-height: 4; 105 | } 106 | 107 | #clear, 108 | #obfuscated { 109 | font-size: 40px; 110 | margin: 0; 111 | } 112 | 113 | h3 { 114 | padding: 30px 0; 115 | } 116 | 117 | select { 118 | width: 100%; 119 | height: 34px; 120 | padding: 6px 12px; 121 | font-size: 14px; 122 | line-height: 1.42857143; 123 | background-color: #fff; 124 | background-image: none; 125 | border: 1px solid #F0F0F1; 126 | border-radius: 4px; 127 | } 128 | 129 | option { 130 | font-weight: normal; 131 | display: block; 132 | padding: 0px 2px 1px; 133 | white-space: pre; 134 | min-height: 1.2em; 135 | } 136 | 137 | pre { 138 | background-color: #FFFFFF; 139 | border: 1px solid #F0F0F1; 140 | padding: 1em; 141 | margin: .5em 0; 142 | overflow: auto; 143 | } 144 | 145 | code { 146 | text-shadow: 0 1px white; 147 | font-family: 'Open Sans', Consolas, Monaco, 'Andale Mono', monospace; 148 | text-align: left; 149 | white-space: pre; 150 | word-spacing: normal; 151 | } 152 | 153 | footer { 154 | background-color: #E1EBF7; 155 | padding: 40px; 156 | float: right; 157 | } 158 | 159 | .github-corner:hover .octo-arm { 160 | animation: octocat-wave 560ms ease-in-out 161 | } 162 | 163 | @keyframes octocat-wave { 164 | 0%, 100% { 165 | transform: rotate(0) 166 | } 167 | 20%, 168 | 60% { 169 | transform: rotate(-25deg) 170 | } 171 | 40%, 172 | 80% { 173 | transform: rotate(10deg) 174 | } 175 | } 176 | 177 | @media (max-width:500px) { 178 | .github-corner:hover .octo-arm { 179 | animation: none 180 | } 181 | .github-corner .octo-arm { 182 | animation: octocat-wave 560ms ease-in-out 183 | } 184 | } 185 | -------------------------------------------------------------------------------- /example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Email Obfuscate, Guard email addresses from being simply scraped by bots. 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 |
23 |
24 |
25 |
26 |

Email Obfuscate

27 | 28 | 29 | 30 | 31 | 32 | 33 |
34 |

Guard email addresses from being simply scraped by bots.

35 |
36 |
37 |
38 |
39 |
40 | 41 |
42 |
43 | 44 |
45 |
46 |
47 |
48 |
49 | Clear: 50 |
51 |
52 | email@obfuscate.js 53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 | Obfuscated: 61 |
62 |
63 |

64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |

Installation:

72 |
73 |
74 |
 npm install email-obfuscate --save
75 |
76 |
77 |

Usage:

78 |
79 |
80 |
import EmailObfuscate from 'email-obfuscate';
 81 | 
 82 | var el = document.getElementById('email');
 83 | 
 84 | EmailObfuscate(el, {
 85 |   // Email construct: name@domain.tld
 86 |   name: 'test',
 87 |   domain: 'example',
 88 |   tld: 'com',
 89 |   // Alternate Text
 90 |   altText: 'Email'
 91 | });
92 |
93 |
94 |

Overview:

95 |
96 |

97 | Email addresses are 98 | harvested from websites using 99 | web scrapers. There are many strategies that can be employed to mitigate scrapers: 100 | 1 101 | 2 102 | 3 103 | 4. 104 |

105 |

However, it must be noted that preventing scraping is an 106 | arms-race. 107 |

108 |

This library's aim to prevent searching for mailto links or using email address regexes.

109 |

It does this by constructing an email address from an object then drawing the text as an image using the canvas.

110 |

For older browsers without a canvas (IE8) we append an element using the alternate text.

111 |
112 |
113 | 116 | 117 | 118 | 119 | 120 | 121 | -------------------------------------------------------------------------------- /lib/emailObfuscate.js: -------------------------------------------------------------------------------- 1 | !function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?exports.EmailObfuscate=t():e.EmailObfuscate=t()}(this,function(){return function(e){function t(i){if(n[i])return n[i].exports;var r=n[i]={exports:{},id:i,loaded:!1};return e[i].call(r.exports,r,r.exports,t),r.loaded=!0,r.exports}var n={};return t.m=e,t.c=n,t.p="",t(0)}([function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}Object.defineProperty(t,"__esModule",{value:!0}),t.EmailObfuscate=void 0;var o=function(){function e(e,t){for(var n=0;n1&&void 0!==arguments[1]?arguments[1]:{};r(this,e),this.el=t;var i=(0,l.default)(d,n);return window.HTMLCanvasElement?this._pseudoElement(i):this._simpleText(i)}return o(e,[{key:"_pseudoElement",value:function(e){var t=new s.default(this.el,e),n=t.determineStyle(),i=new f.default(this.el,n);return i.create()}},{key:"_simpleText",value:function(e){var t=new p.default(this.el,e);return t.create()}}]),e}();t.default=function(e,t){return new y(e,t)}},function(e,t){/* 2 | object-assign 3 | (c) Sindre Sorhus 4 | @license MIT 5 | */ 6 | "use strict";function n(e){if(null===e||void 0===e)throw new TypeError("Object.assign cannot be called with null or undefined");return Object(e)}function i(){try{if(!Object.assign)return!1;var e=new String("abc");if(e[5]="de","5"===Object.getOwnPropertyNames(e)[0])return!1;for(var t={},n=0;n<10;n++)t["_"+String.fromCharCode(n)]=n;var i=Object.getOwnPropertyNames(t).map(function(e){return t[e]});if("0123456789"!==i.join(""))return!1;var r={};return"abcdefghijklmnopqrst".split("").forEach(function(e){r[e]=e}),"abcdefghijklmnopqrst"===Object.keys(Object.assign({},r)).join("")}catch(e){return!1}}var r=Object.getOwnPropertySymbols,o=Object.prototype.hasOwnProperty,a=Object.prototype.propertyIsEnumerable;e.exports=i()?Object.assign:function(e,t){for(var i,l,u=n(e),s=1;s0;e?this.element=this.parent.getElementsByTagName("canvas")[0]:this.element=document.createElement("canvas"),this.context=this.element.getContext("2d"),this._styleCanvas(),e||this.parent.appendChild(this.element)}},{key:"_styleCanvas",value:function(){var e=this.options.width,t=this.options.height,n=new f.default(this.context).calculate();this.element.width=e*n,this.element.height=t*n,this.element.style.width=e+"px",this.element.style.height=t+"px",this.context.scale(n,n),this.element.style.cursor="pointer"}},{key:"_createText",value:function(){if(this.context.textAlign="left",this.context.textBaseline="bottom",this.context.font=this.options.font,this.context.fillStyle=this.options.color,this.context.fillText(this.options.text,0,this.options.height),this.options.underline){var e=this.options.fontSize/10,t=e,n=this.options.height-e-t;this.context.fillRect(0,n,this.options.width,e)}}}]),t}(s.default);t.default=h},function(e,t){"use strict";function n(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}Object.defineProperty(t,"__esModule",{value:!0});var i=function(){function e(e,t){for(var n=0;n0;e?this.element=this.parent.getElementsByTagName("a")[0]:this.element=document.createElement("a"),this.element.innerHTML=this.options.altText,this._styleElement(),e||this.parent.appendChild(this.element)}},{key:"_styleElement",value:function(){this.element.style.cursor="pointer"}}]),t}(s.default);t.default=c}])}); -------------------------------------------------------------------------------- /example/bundle.js: -------------------------------------------------------------------------------- 1 | !function(e){function t(i){if(n[i])return n[i].exports;var o=n[i]={exports:{},id:i,loaded:!1};return e[i].call(o.exports,o,o.exports,t),o.loaded=!0,o.exports}var n={};return t.m=e,t.c=n,t.p="",t(0)}([function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}var o=n(1),r=i(o),a=["Arial","Courier New","Lucida Bright","Palatino","PT Serif","Trebuchet MS","Times New Roman","Verdana"],l=["Abril Fatface","Arvo","Droid Sans","Fira Sans","Josefin Sans","Lato","Old Standard TT","Open Sans","Roboto","Ubuntu","Volkhov"];WebFont.load({google:{families:l},active:function(){a=a.concat(l),u(),f()}});var u=function(){var e=document.getElementById("obfuscated");(0,r.default)(e,{})},c=function(e){var t='.example__a { font-family: "'+e+'" }',n=document.createElement("style");n.appendChild(document.createTextNode(t));var i=document.head.getElementsByTagName("style");i.length>0&&document.head.removeChild(i[0]),document.head.appendChild(n)},s=document.getElementById("fontSelection"),f=function(){for(;s.firstChild;)s.removeChild(s.firstChild);a.map(function(e){var t=document.createElement("option");t.innerHTML=e,t.value=e,s.appendChild(t)})},h=function(e,t){var n=document.getElementById(e).outerHTML,i=document.getElementById(t);i.innerText=n},d=function(){h("clear","clearHtml"),h("obfuscated","obfuscatedHtml")};s.addEventListener("change",function(e){var t=e.target.value;c(t),u()}),window.addEventListener("resize",function(){u()}),u(),f(),d()},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function o(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}Object.defineProperty(t,"__esModule",{value:!0}),t.EmailObfuscate=void 0;var r=function(){function e(e,t){for(var n=0;n1&&void 0!==arguments[1]?arguments[1]:{};o(this,e),this.el=t;var i=(0,l.default)(p,n);return window.HTMLCanvasElement?this._pseudoElement(i):this._simpleText(i)}return r(e,[{key:"_pseudoElement",value:function(e){var t=new c.default(this.el,e),n=t.determineStyle(),i=new f.default(this.el,n);return i.create()}},{key:"_simpleText",value:function(e){var t=new d.default(this.el,e);return t.create()}}]),e}();t.default=function(e,t){return new m(e,t)}},function(e,t){/* 2 | object-assign 3 | (c) Sindre Sorhus 4 | @license MIT 5 | */ 6 | "use strict";function n(e){if(null===e||void 0===e)throw new TypeError("Object.assign cannot be called with null or undefined");return Object(e)}function i(){try{if(!Object.assign)return!1;var e=new String("abc");if(e[5]="de","5"===Object.getOwnPropertyNames(e)[0])return!1;for(var t={},n=0;n<10;n++)t["_"+String.fromCharCode(n)]=n;var i=Object.getOwnPropertyNames(t).map(function(e){return t[e]});if("0123456789"!==i.join(""))return!1;var o={};return"abcdefghijklmnopqrst".split("").forEach(function(e){o[e]=e}),"abcdefghijklmnopqrst"===Object.keys(Object.assign({},o)).join("")}catch(e){return!1}}var o=Object.getOwnPropertySymbols,r=Object.prototype.hasOwnProperty,a=Object.prototype.propertyIsEnumerable;e.exports=i()?Object.assign:function(e,t){for(var i,l,u=n(e),c=1;c0;e?this.element=this.parent.getElementsByTagName("canvas")[0]:this.element=document.createElement("canvas"),this.context=this.element.getContext("2d"),this._styleCanvas(),e||this.parent.appendChild(this.element)}},{key:"_styleCanvas",value:function(){var e=this.options.width,t=this.options.height,n=new f.default(this.context).calculate();this.element.width=e*n,this.element.height=t*n,this.element.style.width=e+"px",this.element.style.height=t+"px",this.context.scale(n,n),this.element.style.cursor="pointer"}},{key:"_createText",value:function(){if(this.context.textAlign="left",this.context.textBaseline="bottom",this.context.font=this.options.font,this.context.fillStyle=this.options.color,this.context.fillText(this.options.text,0,this.options.height),this.options.underline){var e=this.options.fontSize/10,t=e,n=this.options.height-e-t;this.context.fillRect(0,n,this.options.width,e)}}}]),t}(c.default);t.default=h},function(e,t){"use strict";function n(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}Object.defineProperty(t,"__esModule",{value:!0});var i=function(){function e(e,t){for(var n=0;n0;e?this.element=this.parent.getElementsByTagName("a")[0]:this.element=document.createElement("a"),this.element.innerHTML=this.options.altText,this._styleElement(),e||this.parent.appendChild(this.element)}},{key:"_styleElement",value:function(){this.element.style.cursor="pointer"}}]),t}(c.default);t.default=s}]); --------------------------------------------------------------------------------