Skip to content

Commit

Permalink
Add module files to generate HTML, CSS, and JS for an email
Browse files Browse the repository at this point in the history
  • Loading branch information
cduck committed Jun 20, 2020
1 parent 1a367a7 commit 213d999
Show file tree
Hide file tree
Showing 4 changed files with 336 additions and 0 deletions.
3 changes: 3 additions & 0 deletions alime/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@

from .generate import Alime

27 changes: 27 additions & 0 deletions alime/__main__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import argparse
import sys

from .generate import Alime


def main():
parser = argparse.ArgumentParser(
description=
'Generate animated anti-bot email obfuscation HTML+CSS+JS for your '
'website. https://github.com/cduck/alime',
epilog=
'The files alime-example.html, alime.css, and alime.js will be written '
'to the current directory.\n'
'More options are available from the Python code.')
parser.add_argument('email', type=str, help=
'The email address to obfuscate')
args = parser.parse_args()

gen = Alime(email=args.email)
gen.save()

if __name__ == '__main__':
# Fix executable name in help when run with `python3 -m alime`
if len(sys.argv) >= 1 and sys.argv[0].endswith('__main__.py'):
sys.argv[0] = 'python -m alime'
main()
136 changes: 136 additions & 0 deletions alime/generate.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@

from collections import Counter, defaultdict
import html

import math

from . import templates


# Default configuration
PLACEHOLDER = '~' # '~' is a placeholder for remaining characters
TEXT0 = 'scrambled: <~>' # Text in HTML source
TEXT1 = 'Email: <scrambled ~>' # Text shown in browser
TEXT2 = 'Email: placeholder@example.com <~>' # Text shown when hover or click
CHAR_WIDTH = 0.6
ROT_DEGREES = 60


def counter_to_str(counter):
return ''.join(counter.elements())

class Alime:
def __init__(self, email='placeholder@example.com',
text0=TEXT0, text1=TEXT1, text2=TEXT2, placeholder=PLACEHOLDER,
char_width=CHAR_WIDTH, rot_degrees=ROT_DEGREES,
templates=templates):
self.email = email
self.text0 = text0
self.text1 = text1
self.text2 = text2
self.placeholder = placeholder
self.char_width = char_width
self.rot_degrees = ROT_DEGREES
self.templates = templates

self._generate()

def _generate(self):
text2 = self.text2.replace('placeholder@example.com', self.email)

# Count characters
c0 = Counter(self.text0)
c1 = Counter(self.text1)
c2 = Counter(text2)
c0[self.placeholder] = c1[self.placeholder] = c2[self.placeholder] = 0

# Find the minimum set of characters to represent the three strings
c_all = Counter()
for char in sorted(set(c0) | set(c1) | set(c2)):
c_all[char] = max(c0[char], c1[char], c2[char])

# Determine output character order
c_temp = Counter(c_all)
c_rest = c_all - c0
str_rest= ''.join(sorted(counter_to_str(c_rest)))
char_order = self.text0.replace(self.placeholder, str_rest)
char_order, len(char_order)

# Calculate character positions for 1 and 2
def calc_positions(text):
char_search_index = defaultdict(int)
unused_index = text.find(self.placeholder)
positions = [None] * len(char_order)
for i, char in enumerate(char_order):
idx = text.find(char, char_search_index[char])
if idx < 0:
positions[i] = unused_index
else:
positions[i] = idx
char_search_index[char] = idx + 1
return positions, unused_index
positions1, unused_index1 = calc_positions(self.text1)
positions2, unused_index2 = calc_positions(text2)

# Calculate text width
max_width = max(len(self.text0), len(self.text1), len(text2))
em_width = round(max_width * self.char_width, 6)

# Generate HTML
self.generated_chars = ''.join(
self.templates.CHAR.format(char=html.escape(char))
for char in char_order
)
self.generated_html = self.templates.HTML.format(
width=em_width,
chars=self.generated_chars,
)

# Generate CSS
def dist_to_yoff(dist):
rot_rad = math.radians(self.rot_degrees)
return dist / 2 / math.tan(rot_rad/2)
self.css_rep=''.join(
self.templates.CSS_REP.format(
i=i+2,
left=round((positions1[i]-i)*self.char_width, 6),
origin=round((positions2[i]-positions1[i]+1)/2*self.char_width,
6),
yoff=round(0.5+dist_to_yoff(
(positions2[i]-positions1[i])*self.char_width),
6),
other=' display: none;' if (positions1[i] == unused_index1
and positions2[i] == unused_index2) else '',
)
for i in range(len(char_order))
)
self.generated_css = self.templates.CSS.format(
pos_deg=self.rot_degrees,
neg_deg=-self.rot_degrees,
rep=self.css_rep,
)

# JavaScript
self.generated_js = self.templates.JAVASCRIPT_STATIC

# Generate example page
self.generated_page = self.templates.EXAMPLE_PAGE.format(
html=self.generated_html)

def save(self, html_fname='alime-example.html', css_fname='alime.css',
js_fname='alime.js'):
self.save_page(html_fname)
self.save_css(css_fname)
self.save_js(js_fname)

def save_page(self, fname='alime-example.html'):
with open(fname, 'w') as f:
f.write(self.generated_page)

def save_css(self, fname='alime.css'):
with open(fname, 'w') as f:
f.write(self.generated_css)

def save_js(self, fname='alime.js'):
with open(fname, 'w') as f:
f.write(self.generated_js)
170 changes: 170 additions & 0 deletions alime/templates.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
HTML = '''\
<a href="#javascript-disabled" class="alime" style="width: {width}em">
<span class="sr-only">(Click to send mail)</span>
<!-- Automatically generated (https://github.com/cduck/alime) -->
{chars}
</a>\
'''
CHAR = '<span><span>{char}</span></span>'

CSS = '''\
/* This file was automatically generated (https://github.com/cduck/alime) */
.alime, .alimestatic {{
display: inline-block;
white-space: nowrap;
padding: 0;
font-family: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", \
"Courier New", monospace !important;
}}
.alime > *:nth-child(n+2) {{
display: inline-block;
padding: 0;
margin: 0;
width: 0.6em;
height: 1em;
text-align: center;
position: relative;
left: 0em;
transform-origin: center center;
transition-duration: 1s;
transition-property: transform;
}}
.alime > * > * {{
display: inline-block;
padding: 0;
margin: 0;
width: 0.6em;
height: 1em;
text-align: center;
transform-origin: center center;
transition-duration: 1s;
transition-property: transform;
}}
.alime:hover > *,
.alime:active > *,
.alime:focus > *,
.alime.alimeanim > * {{
transform: rotate({pos_deg}deg);
}}
.alime.alimeclicked > * {{
transform: rotate({pos_deg}deg);
transition-duration: 0;
transition-property: none;
}}
.alime:hover > * > *,
.alime:active > * > *,
.alime:focus > * > *,
.alime.alimeanim > * > * {{
transform: rotate({neg_deg}deg);
}}
.alime.alimeclicked > * > * {{
transform: rotate({neg_deg}deg);
transition-duration: 0;
transition-property: none;
}}
.alime > .sr-only, .alimestatic > .sr-only {{
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}}
{rep}\
'''
CSS_REP = '''\
.alime > :nth-child({i}) {{\
left: {left}em; \
transform-origin: {origin}em {yoff}em;\
{other}\
}}
'''

JAVASCRIPT_STATIC = '''\
/* This file was automatically generated (https://github.com/cduck/alime) */
function alime_load() {
let elems = document.getElementsByClassName("alime");
for (var i=0; i < elems.length; i++) {
elems[i].href = "#click-to-unscramble";
elems[i].onclick = alime_click_handler(elems[i]);
}
}
function alime_click_handler(elem) {
function alime_clicked(e) {
// Immediately complete animation
elem.classList.add("alimeclicked");
// Unscramble
let whole_str = Array.prototype.map.call(elem.children,
function(val, i) {
return [val.getBoundingClientRect().x, val];
}).sort(function(a, b) {
return a[0]-b[0];
}).map(function(pair) {
if (pair[1].classList.contains("sr-only")) {
return "";
}
pair[1].style.display = "none";
return pair[1].innerText || " ";
}).join("");
// Add plain clickable link
let link_text = (
whole_str.substring(1, whole_str.indexOf(":"))
+ "to"
+ whole_str.substring(whole_str.indexOf(":"), whole_str.indexOf("<"))
).trim().replace(" ", "");
let print_text = whole_str.substring(whole_str.indexOf("<"), -1).trim();
elem.classList.remove("alime");
elem.classList.remove("alimeclicked");
elem.classList.add("alimestatic");
let static_elem = document.createElement("a");
static_elem.innerText = print_text;
static_elem.href = link_text;
elem.href = link_text;
elem.appendChild(static_elem);
elem.onclick = null;
}
return alime_clicked;
}
alime_load();
'''

EXAMPLE_PAGE = '''\
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Alime Example Web Page</title>
<!-- Include the following line between <head></head> in your page -->
<link rel="stylesheet" type="text/css" href="alime.css">
</head>
<body>
<h2>Alime Example Web Page</h2>
<p>
<!-- Include the following lines in your page where you want your address -->
{html}
<!-- End include -->
</p>
<!-- Include the following line at the very end of <body></body> in your page \
(optional) -->
<script src='alime.js'></script>
</body>
</html>
'''

0 comments on commit 213d999

Please sign in to comment.