@cull/pore
Version:
An email data analysis library.
98 lines (92 loc) • 3 kB
text/typescript
import SankeyGraph, { SankeyNode } from './sankey';
import Data from './data';
/**
* Datum collection analyzing the origins of email.
*/
export default class Origin extends Data {
/**
* Distinct sender values
* e.g. `["jon <jon@mail.example.com>",...]`
*/
senders!: Set<string>;
/**
* Distinct addresses
* e.g. `["jon@mail.example.com",...]`
*/
addresses!: Set<string>;
/**
* Distinct domains
* e.g. `["example.com",...]`
*/
domains!: Set<string>;
/**
* Compute properties dependent on filtered data.
*/
protected compute(): void {
super.compute();
/**
* Though seemingly uncommon in practice, technically there may be more than one "from" header.
* Currently, treat each value as a distinct message. A natural side effect of this approach is that
* counting distinct "from" values may be greater than the total number of distinct Datum.
*
* @todo Is there a better way to handle the scenario where more than one "from" value is present?
* @todo Should the "from" values be filtered to guarantee uniqueness in the context of Datum?
*/
this.senders = new Set();
this.addresses = new Set();
this.domains = new Set();
this.data
.flatMap(datum => datum.from)
.forEach(identity => {
this.senders.add(identity.toString());
this.addresses.add(identity.address);
this.domains.add(identity.domain);
});
}
/**
* Generate Sankey graph nodes and links for email origins against filtered data.
*
* @todo benchmark
* @todo seems like the kind of processing that could move to worker?
*/
sankey(): SankeyGraph {
let graph = new SankeyGraph();
this.senders.forEach(sender => {
let data = this.data.filter(d => d.from.find(f => f.toString() === sender));
if (data.length > 0) {
let first = data[0].from.find(f => f.toString() === sender);
if (first !== undefined) {
let address = first.address;
let source = createNode({ sender });
let target = createNode({ address });
graph.append({ source, target, data });
}
}
});
this.addresses.forEach(address => {
let data = this.data.filter(d => d.from.find(f => f.address === address));
if (data.length > 0) {
let first = data[0].from.find(f => f.address === address);
if (first !== undefined) {
let domain = first.domain;
let source = createNode({ address });
let target = createNode({ domain });
graph.append({ source, target, data });
}
}
});
return graph;
}
}
/**
* Convenience utility to create a SankeyNode entity from a terse object as input.
*/
export let createNode = (
n: { sender: string } | { address: string } | { domain: string }
): SankeyNode => {
let kind = Object.keys(n)[0];
let label = n[kind];
let id = `${kind}__${label}`;
let node = { kind, label, id };
return node;
};