1
0
mirror of https://github.com/MailHops/mailhops-plugin.git synced 2025-05-16 14:20:10 -07:00
mailhops-plugin/js/mailhops.js
Mihai Moldovan 72ac705df2 {content/preferences.html,js/{mailhops,preferences}.js}: prepare option for extra information.
The current code only shows the basic auth status and a description of
what it means. Some users (i.e., me) would like to see more information,
typically the whole header part that is responsible for the decision, so
prepare to show this information.

This commit does NOT hook it up just yet. This will be done in a later
commit.
2024-04-17 11:47:59 +02:00

468 lines
17 KiB
JavaScript

/*
* @author: Andrew Van Tassel
* @email: andrew@andrewvantassel.com
* @website: http://Mailhops.com
*/
class MailHops {
msgURI = null
isLoaded = false
loading = false
tabId = null
options = {
version: 'MailHops Plugin 4.4.0',
api_key: '',
owm_key: '',
lang: 'en',
unit: 'mi',
theme: 'light',
api_http: 'https://',
api_host: 'api.Mailhops.com',
travel_time_junk: false,
extrainfo: false,
debug: false,
country_filter: []
}
message = {
id: null
, map_url: ''
, time: null
, date: new Date().toISOString()
, hash: ''
, secure: []
, headers: []
, auth: []
, sender: {
icon: '/images/refresh.png'
, title: 'Loading...'
, description: ''
},
error: ''
}
response = {}
LOG(msg) {
if (!this.options.debug)
return;
console.log(msg);
}
async init(tabId, id, headers) {
this.tabId = tabId;
try {
var data = await browser.storage.local.get();
if (data.api_key) {
this.options.api_key = data.api_key;
}
if (data.owm_key) {
this.options.owm_key = data.owm_key;
}
if (data.lang) {
this.options.lang = data.lang;
}
if (data.unit) {
this.options.unit = data.unit;
}
if (data.theme) {
this.options.theme = data.theme;
}
if (data.travel_time_junk && data.travel_time_junk != 'off') {
this.options.travel_time_junk = Boolean(data.travel_time_junk);
}
if (data.extrainfo) {
this.options.extrainfo = Boolean(data.extrainfo);
}
if (data.debug) {
this.options.debug = Boolean(data.debug);
}
if (data.countries) {
this.options.country_filter = data.countries.split(',');
}
this.LOG('load MailHops prefs');
// reset message
this.message = {
id: id
, map_url: ''
, time: null
, date: new Date().toISOString()
, hash: ''
, secure: []
, headers: headers
, auth: []
, sender: {
icon: '/images/refresh.png'
, title: 'Loading...'
, description: ''
},
error: ''
};
this.getRoute();
} catch (e) {
this.LOG('Error loading MailHops prefs');
}
}
async getRoute() {
if (this.loading) return;
this.loading = true;
// set loading icon
browser.messageDisplayAction.setPopup({ popup: '', tabId: this.tabId });
browser.messageDisplayAction.setIcon({ path: '/images/refresh.png', tabId: this.tabId });
browser.messageDisplayAction.setTitle({ title: 'Loading...', tabId: this.tabId });
//IP regex
var regexIp = /(1\d{0,2}|2(?:[0-4]\d{0,1}|[6789]|5[0-5]?)?|[3-9]\d?|0)\.(1\d{0,2}|2(?:[0-4]\d{0,1}|[6789]|5[0-5]?)?|[3-9]\d?|0)\.(1\d{0,2}|2(?:[0-4]\d{0,1}|[6789]|5[0-5]?)?|[3-9]\d?|0)\.(1\d{0,2}|2(?:[0-4]\d{0,1}|[6789]|5[0-5]?)?|[3-9]\d?|0)(\/(?:[012]\d?|3[012]?|[456789])){0,1}$/;
var regexAllIp = /(1\d{0,2}|2(?:[0-4]\d{0,1}|[6789]|5[0-5]?)?|[3-9]\d?|0)\.(1\d{0,2}|2(?:[0-4]\d{0,1}|[6789]|5[0-5]?)?|[3-9]\d?|0)\.(1\d{0,2}|2(?:[0-4]\d{0,1}|[6789]|5[0-5]?)?|[3-9]\d?|0)\.(1\d{0,2}|2(?:[0-4]\d{0,1}|[6789]|5[0-5]?)?|[3-9]\d?|0)(\/(?:[012]\d?|3[012]?|[456789])){0,1}/g;
var regexIPV6 = /s*((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3})|:))|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3})|:))|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:)))(%.+)?s*/g;
var headReceived = this.message.headers['received'] || [];
var headDate = this.message.headers['date'] ? this.message.headers['date'][0] : '';
var headXReceived = this.message.headers['x-received'] ? this.message.headers['x-received'][0] : '';
var headXOrigIP = this.message.headers['x-originating-ip'] ? this.message.headers['x-originating-ip'][0] : '';
// auth box
var headXMailer = this.message.headers['x-mailer'] ? this.message.headers['x-mailer'][0] : '';
var headUserAgent = this.message.headers['user-agent'] ? this.message.headers['user-agent'][0] : '';
var headXMimeOLE = this.message.headers['x-mimeole'] ? this.message.headers['x-mimeole'][0] : '';
var headReceivedSPF = this.message.headers['received-spf'] ? this.message.headers['received-spf'][0] : '';
var headAuth = this.message.headers['authentication-results'] ? this.message.headers['authentication-results'][0] : '';
var headListUnsubscribe = this.message.headers['list-unsubscribe'] ? this.message.headers['list-unsubscribe'][0] : '';
var all_ips = new Array();
var rline = '';
var firstDate = headDate;
var lastDate;
//empty secure and time
this.message.secure = [];
this.message.time = null;
try {
this.message.date = new Date(headDate).toISOString();
} catch (error) {
headDate = headDate.substring(0, headDate.lastIndexOf(' '));
}
try {
this.message.date = new Date(headDate).toISOString();
} catch (error) {
headDate = new Date();
}
this.message.auth = this.auth(headXMailer, headUserAgent, headXMimeOLE, headAuth, headReceivedSPF, headListUnsubscribe);
//loop through the received headers and parse for IP addresses
if (Boolean(headReceived)) {
var received_ips = new Array();
for (var h = 0; h < headReceived.length; h++) {
//build the received line by concat until semi-colon ; date/time
rline += headReceived[h];
if (headReceived[h].indexOf(';') === -1)
continue;
// first and last dates are used to calculate time traveled
if (rline.indexOf(';') !== -1) {
if (!firstDate)
firstDate = rline.substring(rline.indexOf(';') + 1).trim();
if (!lastDate)
lastDate = rline.substring(rline.indexOf(';') + 1).trim();
}
// IPV6 check
rline = rline.replace(/\[IPv6\:/g, '[');
if (rline.match(regexIPV6)) {
all_ips.unshift(rline.match(regexIPV6)[0]);
//reset the line
rline = '';
continue;
}
// parse IPs out of Received line
received_ips = rline.match(regexAllIp);
//continue if no IPs found
if (!received_ips) {
//reset the line
rline = '';
continue;
}
//get unique IPs for each Received header
received_ips = received_ips.filter(function (item, pos) {
return received_ips.indexOf(item) === pos;
});
for (var r = received_ips.length; r >= 0; r--) {
if (regexIp.test(received_ips[r]) && this.testIP(received_ips[r], rline)) {
all_ips.unshift(received_ips[r]);
}
}
//reset the line
rline = '';
}
}
// parse dates
if (firstDate && firstDate.indexOf('(') !== - 1)
firstDate = firstDate.substring(0, firstDate.indexOf('(')).trim();
if (lastDate && lastDate.indexOf('(') !== -1)
lastDate = lastDate.substring(0, lastDate.indexOf('(')).trim();
if (firstDate && lastDate) {
try {
firstDate = new Date(firstDate);
lastDate = new Date(lastDate);
this.message.time = lastDate - firstDate;
} catch (e) {
this.LOG('travel dates parse Error: ' + JSON.stringify(e));
this.message.time = null;
}
} else {
this.message.time = null;
}
//get the originating IP address
if (Boolean(headXOrigIP)) {
headXOrigIP = headXOrigIP.replace(/\[IPv6\:/g, '[');
//IPV6 check
if (headXOrigIP.match(regexIPV6)) {
var ip = headXOrigIP.match(regexIPV6)
if (Boolean(ip) && ip.length && all_ips.indexOf(ip[0]) == -1)
all_ips.unshift(ip[0]);
} else {
var ip = headXOrigIP.match(regexAllIp);
if (Boolean(ip) && ip.length && all_ips.indexOf(ip[0]) == -1)
all_ips.unshift(ip[0]);
}
}
if (all_ips.length) {
// set the message hash
this.message.hash = btoa(this.message.date + '' + all_ips.join(','));
const cached = await this.getCacheResponse();
if (cached) {
this.displayRoute(cached);
this.isLoaded = true;
this.loading = false;
} else {
this.lookupRoute(all_ips.join(','));
}
} else {
this.clear();
}
};
//another ip check, dates will throw off the regex
testIP(ip, header) {
var validIP = true;
try {
var firstchar = header.substring(header.indexOf(ip) - 1);
firstchar = firstchar.substring(0, 1);
var lastchar = header.substring((header.indexOf(ip) + ip.length));
lastchar = lastchar.substring(0, 1);
if (firstchar.match(/\.|\d|\-/)
|| lastchar.match(/\.|\d|\-/)
|| (firstchar == '?' && lastchar == '?')
|| (firstchar == ':' || lastchar == ':')
|| lastchar == ';'
|| header.toLowerCase().indexOf(' id ' + ip) !== -1
|| parseInt(ip.substring(0, ip.indexOf('.'))) >= 240 //IANA-RESERVED
) {
//only if there is one instance of this IP
if (header.indexOf(ip) == header.lastIndexOf(ip))
validIP = false;
} else if (header.indexOf('using SSL') !== -1
|| header.indexOf('using TLS') !== -1
|| header.indexOf('version=TLSv1/SSLv3') !== -1
) {
//check if this IP was part of a secure transmission
this.message.secure.push(ip);
}
} catch (e) {
this.LOG('testIP Error: ' + JSON.stringify(e));
}
return validIP;
}
clear() {
this.message.sender = {
title: 'Local',
countryCode: '',
icon: '/images/local.png'
};
browser.messageDisplayAction.setIcon({ path: this.message.sender.icon, tabId: this.tabId });
browser.messageDisplayAction.setTitle({ title: this.message.sender.title, tabId: this.tabId });
this.isLoaded = true;
this.loading = false;
}
error(status, data) {
this.message.error = (data && data.error && data.error.message) ? data && data.error.message : 'Service Unavailable';
this.message.sender = {
title: (data && data.error && data.error.message) ? data && data.error.message : 'Service Unavailable',
countryCode: '',
icon: '/images/auth/error.png'
};
browser.messageDisplayAction.setIcon({ path: this.message.sender.icon, tabId: this.tabId });
browser.messageDisplayAction.setTitle({ title: this.message.sender.title, tabId: this.tabId });
}
auth(header_xmailer, header_useragent, header_xmimeole, header_auth, header_spf, header_unsubscribe) {
let auth = [];
//SPF
if (header_spf) {
header_spf = header_spf.replace(/^\s+/, "");
var headerSPFArr = header_spf.split(' ');
auth.push({
type: 'SPF',
color: 'green',
icon: '/images/auth/' + headerSPFArr[0] + '.png',
copy: header_spf + '\n' + MailHopsUtils.spf(headerSPFArr[0]).trim()
});
}
//Authentication-Results
//http://tools.ietf.org/html/rfc5451
if (header_auth) {
var headerAuthArr = header_auth.split(';');
var dkim_result;
var spf_result;
for (var h = 0; h < headerAuthArr.length; h++) {
if (headerAuthArr[h].indexOf('dkim=') != -1) {
dkim_result = headerAuthArr[h];
if (header_spf)
break;
}
if (!header_spf && headerAuthArr[h].indexOf('spf=') != -1) {
spf_result = headerAuthArr[h];
if (dkim_result)
break;
}
}
if (dkim_result) {
dkim_result = dkim_result.replace(/^\s+/, "");
var dkimArr = dkim_result.split(' ');
auth.push({
type: 'DKIM',
color: 'green',
icon: '/images/auth/' + dkimArr[0].replace('dkim=', '') + '.png',
copy: dkim_result + '\n' + MailHopsUtils.dkim(dkimArr[0].replace('dkim=', '')).trim()
});
}
if (spf_result) {
spf_result = spf_result.replace(/^\s+/, "");
var spfArr = spf_result.split(' ');
auth.push({
type: 'SPF',
color: 'green',
icon: '/images/auth/' + spfArr[0].replace('spf=', '') + '.png',
copy: spf_result + '\n' + MailHopsUtils.spf(spfArr[0].replace('spf=', '')).trim()
});
}
}
if (header_unsubscribe) {
auth.push({
type: 'Unsubscribe',
color: 'grey',
link: header_unsubscribe.replace('<', '').replace('>', '').trim()
});
}
return auth;
}
//mailhops lookup
lookupRoute(header_route) {
let mailHop = this;
let lookupURL = '?' + MailHopsUtils.getAPIUrlParams(this.options) + '&r=' + String(header_route) + '&l=' + this.options.lang + '&u=' + this.options.unit;
if (this.options.owm_key != '')
lookupURL += '&owm_key=' + this.options.owm_key;
if (this.message.time != null)
lookupURL += '&t=' + this.message.time;
if (this.message.date != null)
lookupURL += '&d=' + this.message.date;
this.message.map_url = MailHopsUtils.getAPIUrl() + '/map/' + lookupURL;
//call mailhops api for lookup
var xmlhttp = new XMLHttpRequest();
xmlhttp.open("GET", MailHopsUtils.getAPIUrl() + '/lookup/' + lookupURL, true);
xmlhttp.onreadystatechange = function () {
if (xmlhttp.readyState === 4) {
try {
let data = JSON.parse(xmlhttp.responseText);
if (xmlhttp.status === 200) {
mailHop.cacheResponse(data.response);
mailHop.displayRoute(data.response);
//tag the result
mailHop.tagResults(data, data.response.route);
} else if (data.error) {
mailHop.LOG(JSON.stringify(data.error));
//display the error
mailHop.error(xmlhttp.status, data);
}
} catch (e) {
mailHop.LOG(e);
mailHop.error();
}
}
mailHop.isLoaded = true;
mailHop.loading = false;
};
xmlhttp.send(null);
}
displayRoute(response) {
this.response = response;
this.message.sender = MailHopsUtils.getSender(response.route);
if (this.message.sender) {
browser.messageDisplayAction.setIcon({ path: this.message.sender.icon, tabId: this.tabId });
browser.messageDisplayAction.setTitle({ title: this.message.sender.title, tabId: this.tabId });
} else {
browser.messageDisplayAction.setIcon({ path: '/images/local.png', tabId: this.tabId });
browser.messageDisplayAction.setTitle({ title: 'Local', tabId: this.tabId });
}
}
// keep a daily cache
async cacheResponse(response) {
let data = await browser.storage.local.get('messages');
let cached_date = new Date();
let messages = {
cached: cached_date.getUTCFullYear() + '-' + cached_date.getUTCMonth() + '-' + cached_date.getUTCDate(),
list: []
};
if (data.messages && data.messages.list && data.messages.cached === messages.cached) {
messages.list = data.messages.list;
}
messages.list[this.message.hash] = response;
await browser.storage.local.set({ messages: messages });
this.LOG('Cached Message ' + this.message.id + ' hash ' + this.message.hash);
}
// get cached message
async getCacheResponse() {
let data = await browser.storage.local.get('messages');
if (data.messages && data.messages.list[this.message.hash]) {
this.LOG('Found Cached Message ' + this.message.hash);
return data.messages.list[this.message.hash];
}
return false;
};
tagResults(results, route) {
if (!results) {
return false;
}
//Add junk tag on messages taking too long to travel
try {
if (Boolean(this.options.travel_time_junk) && this.message.time != null && this.message.time > 10000) {
messenger.messages.update(this.message.id, { 'junk': true });
this.LOG("Junk: Travel time match for " + this.message.time);
}
if (this.options.country_filter.length && this.message.sender && this.message.sender.countryCode) {
if (this.options.country_filter.indexOf(this.message.sender.countryCode.toUpperCase()) != -1) {
messenger.messages.update(this.message.id, { 'junk': true });
this.LOG("Junk: Country code match for " + this.message.sender.countryCode);
}
}
} catch (e) {
this.LOG("Error tagResults: " + e);
}
}
}