Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

@salesflare/planer

Package Overview
Dependencies
Maintainers
3
Versions
3
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@salesflare/planer - npm Package Compare versions

Comparing version 1.1.3 to 2.0.0

57

lib/htmlPlaner.js

@@ -1,2 +0,2 @@

// Generated by CoffeeScript 1.12.7
// Generated by CoffeeScript 2.5.1
(function() {

@@ -9,16 +9,20 @@ var BREAK_TAG_REGEX, CHECKPOINT_PREFIX, CHECKPOINT_SUFFIX, DOCUMENT_POSITION_FOLLOWING, DOCUMENT_POSITION_PRECEDING, OUTLOOK_SPLITTER_QUERY_SELECTORS, OUTLOOK_SPLITTER_QUOTE_IDS, OUTLOOK_XPATH_SPLITTER_QUERIES, QUOTE_IDS, compareByDomPosition, elementIsAllContent, ensureTextNodeBetweenChildElements, findMicrosoftSplitter, findOutlookSplitterWithQuerySelector, findOutlookSplitterWithQuoteId, findOutlookSplitterWithXpathQuery, findParentDiv, hasTagName, isTextNodeWrappedInSpan, removeNodes;

exports.CHECKPOINT_PATTERN = new RegExp(CHECKPOINT_PREFIX + "\\d+" + CHECKPOINT_SUFFIX, 'g');
exports.CHECKPOINT_PATTERN = new RegExp(`${CHECKPOINT_PREFIX}\\d+${CHECKPOINT_SUFFIX}`, 'g');
// HTML quote indicators (tag ids)
QUOTE_IDS = ['OLK_SRC_BODY_SECTION'];
// Create an instance of Document using the message html and the injected base document
exports.createEmailDocument = function(msgBody, dom) {
var emailBodyElement, emailDocument, head, htmlElement;
emailDocument = dom.implementation.createHTMLDocument();
htmlElement = emailDocument.getElementsByTagName('html')[0];
// Write html of email to `html` element
[htmlElement] = emailDocument.getElementsByTagName('html');
htmlElement.innerHTML = msgBody.trim();
if (emailDocument.body == null) {
emailBodyElement = emailDocument.getElementsByTagName('body')[0];
[emailBodyElement] = emailDocument.getElementsByTagName('body');
emailDocument.body = emailBodyElement;
}
head = emailDocument.getElementsByTagName('head')[0];
// Remove 'head' element from document
[head] = emailDocument.getElementsByTagName('head');
if (head) {

@@ -30,12 +34,17 @@ emailDocument.documentElement.removeChild(head);

// Recursively adds checkpoints to html tree.
exports.addCheckpoints = function(htmlNode, counter) {
var childNode, i, len, ref;
// 3 is a text node
if (htmlNode.nodeType === 3) {
htmlNode.nodeValue = "" + (htmlNode.nodeValue.trim()) + CHECKPOINT_PREFIX + counter + CHECKPOINT_SUFFIX + "\n";
htmlNode.nodeValue = `${htmlNode.nodeValue.trim()}${CHECKPOINT_PREFIX}${counter}${CHECKPOINT_SUFFIX}\n`;
counter++;
}
// 1 is an element
if (htmlNode.nodeType === 1) {
if (!hasTagName(htmlNode, 'body')) {
htmlNode.innerHTML = " " + htmlNode.innerHTML + " ";
// Pad with spacing to ensure there are text nodes at the begining and end of non-body elements
htmlNode.innerHTML = ` ${htmlNode.innerHTML} `;
}
// Ensure that there are text nodes between sibling elements
ensureTextNodeBetweenChildElements(htmlNode);

@@ -52,4 +61,5 @@ ref = htmlNode.childNodes;

exports.deleteQuotationTags = function(htmlNode, counter, quotationCheckpoints) {
var childNode, childTagInQuotation, i, j, len, len1, quotationChildren, ref, ref1, tagInQuotation;
var childNode, childTagInQuotation, i, j, len, len1, quotationChildren, ref, tagInQuotation;
tagInQuotation = true;
// 3 is a text node
if (htmlNode.nodeType === 3) {

@@ -62,8 +72,12 @@ if (!quotationCheckpoints[counter]) {

}
// 1 is an element
if (htmlNode.nodeType === 1) {
// Collect child nodes that are marked as in the quotation
childTagInQuotation = false;
quotationChildren = [];
if (!hasTagName(htmlNode, 'body')) {
htmlNode.innerHTML = " " + htmlNode.innerHTML + " ";
// Pad with spacing to ensure there are text nodes at the begining and end of non-body elements
htmlNode.innerHTML = ` ${htmlNode.innerHTML} `;
}
// Ensure that there are text nodes between sibling elements
ensureTextNodeBetweenChildElements(htmlNode);

@@ -73,3 +87,4 @@ ref = htmlNode.childNodes;

childNode = ref[i];
ref1 = exports.deleteQuotationTags(childNode, counter, quotationCheckpoints), counter = ref1[0], childTagInQuotation = ref1[1];
[counter, childTagInQuotation] = exports.deleteQuotationTags(childNode, counter, quotationCheckpoints);
// Keep tracking if all children are in the quotation
tagInQuotation = tagInQuotation && childTagInQuotation;

@@ -81,5 +96,7 @@ if (childTagInQuotation) {

}
// If all of an element's children are part of a quotation, let parent delete whole element
if (tagInQuotation) {
return [counter, tagInQuotation];
} else {
// Otherwise, delete specific quotation children
for (j = 0, len1 = quotationChildren.length; j < len1; j++) {

@@ -119,2 +136,3 @@ childNode = quotationChildren[j];

// Remove the last non-nested blockquote element
exports.cutBlockQuote = function(emailDocument) {

@@ -150,4 +168,6 @@ var blockquoteElement, div, parent, xpathQuery, xpathResult;

var afterSplitter, fromBlock, lastBlock, parentDiv, ref, splitterElement, textNode, xpathQuery, xpathResult;
// Handle case where From: block is enclosed in a tag
xpathQuery = "//*[starts-with(normalize-space(.), 'From:')]|//*[starts-with(normalize-space(.), 'Date:')]";
xpathResult = emailDocument.evaluate(xpathQuery, emailDocument, null, 5, null);
// Find last element in iterator
while (fromBlock = xpathResult.iterateNext()) {

@@ -157,2 +177,3 @@ lastBlock = fromBlock;

if (lastBlock != null) {
// Find parent div and remove from document
parentDiv = findParentDiv(lastBlock);

@@ -164,4 +185,6 @@ if ((parentDiv != null) && !elementIsAllContent(parentDiv)) {

}
// Handle the case when From: block goes right after e.g. <hr> and is not enclosed in a tag itself
xpathQuery = "//text()[starts-with(normalize-space(.), 'From:')]|//text()[starts-with(normalize-space(.), 'Date:')]";
xpathResult = emailDocument.evaluate(xpathQuery, emailDocument, null, 9, null);
// The text node that is the result
textNode = xpathResult.singleNodeValue;

@@ -172,4 +195,7 @@ if (textNode == null) {

if (isTextNodeWrappedInSpan(textNode)) {
// The text node is wrapped in a span element. All sorts formatting could be happening here.
// Return false and hope plain text algorithm can figure it out.
return false;
}
// The previous sibling stopped the initial xpath query from working, so it is likely a splitter (like an hr)
splitterElement = textNode.previousSibling;

@@ -181,2 +207,3 @@ if (splitterElement != null) {

}
// Remove all subsequent siblings of the textNode
afterSplitter = textNode.nextSibling;

@@ -222,2 +249,4 @@ while (afterSplitter != null) {

// Queries to find a splitter that's the only child of a single parent div
// Usually represents the dividing line between messages in the Outlook html
OUTLOOK_SPLITTER_QUERY_SELECTORS = {

@@ -229,2 +258,3 @@ outlook2007: "div[style='border:none;border-top:solid #B5C4DF 1.0pt;padding:3.0pt 0cm 0cm 0cm']",

// More complicated Xpath queries for versions of Outlook that don't use the dividing lines
OUTLOOK_XPATH_SPLITTER_QUERIES = {

@@ -234,3 +264,5 @@ outlook2003: "//div/div[@class='MsoNormal' and @align='center' and @style='text-align:center']/font/span/hr[@size='3' and @width='100%' and @align='center' and @tabindex='-1']"

// For more modern versions of Outlook that contain replies in quote block with an id
OUTLOOK_SPLITTER_QUOTE_IDS = {
// There's potentially multiple elements with this id so we need to cut everything after this quote as well
office365: '#divRplyFwdMsg'

@@ -263,2 +295,3 @@ };

}
// Find the earliest splitter in the DOM to remove everything after it
return possibleSplitterElements.sort(compareByDomPosition)[0];

@@ -286,2 +319,3 @@ };

splitterElement = xpathResult.singleNodeValue;
// Go up the tree to find the enclosing div.
if (splitterElement != null) {

@@ -319,3 +353,3 @@ splitterElement = splitterElement.parentElement.parentElement;

results = [];
for (index = i = ref = nodesArray.length - 1; ref <= 0 ? i <= 0 : i >= 0; index = ref <= 0 ? ++i : --i) {
for (index = i = ref = nodesArray.length - 1; (ref <= 0 ? i <= 0 : i >= 0); index = ref <= 0 ? ++i : --i) {
node = nodesArray[index];

@@ -338,2 +372,3 @@ results.push(node != null ? (ref1 = node.parentNode) != null ? ref1.removeChild(node) : void 0 : void 0);

while (currentNode.nextSibling) {
// An element is followed by an element
if (currentNode.nodeType === 1 && currentNode.nextSibling.nodeType === 1) {

@@ -340,0 +375,0 @@ newTextNode = dom.createTextNode(' ');

145

lib/planer.js

@@ -1,2 +0,2 @@

// Generated by CoffeeScript 1.12.7
// Generated by CoffeeScript 2.5.1
(function() {

@@ -15,9 +15,11 @@ var CONTENT_CHUNK_SIZE, MAX_LINES_COUNT, MAX_LINE_LENGTH, REGEXES, SPLITTER_MAX_LINES, _CRLF_to_LF, _restore_CRLF, getDelimiter, htmlPlaner, isSplitter, postprocess, preprocess, setReturnFlags;

exports.extractFrom = function(msgBody, contentType, dom) {
if (contentType == null) {
contentType = 'text/plain';
}
if (dom == null) {
dom = null;
}
// Extract actual message from email.
// Will use provided `contentType` to decide which algorithm to use (plain text or html).
// @param msgBody [String] the html content of the email
// @param contentType [String] the contentType of the email. Only `text/plain` and `text/html` are supported.
// @param dom [Document] the document object to use for html parsing.
// @return [String] the text/html of the actual message without quotations
exports.extractFrom = function(msgBody, contentType = 'text/plain', dom = null) {
if (contentType === 'text/plain') {

@@ -33,2 +35,13 @@ return exports.extractFromPlain(msgBody);

// Extract actual message from provided textual email.
// Store delimiter used by the email (\n or \r\n),
// split the email into lines,
// use regexes to mark each line as either part of the message or quotation,
// remove lines that are part of the quotation,
// put message back together using the saved delimeter,
// remove changes made by algorithm.
// @param msgBody [String] the html content of the email
// @return [String] the text of the message without quotations
exports.extractFromPlain = function(msgBody) {

@@ -46,4 +59,25 @@ var delimiter, lines, markers;

// Extract actual message from provided html message body
// using tags and plain text algorithm.
// Cut out the 'blockquote', 'gmail_quote' tags.
// Cut out Microsoft (Outlook, Windows mail) quotations.
// Then use plain text algorithm to cut out splitter or
// leftover quotation.
// This works by adding checkpoint text to all html tags,
// then converting html to text,
// then extracting quotations from text,
// then checking deleted checkpoints,
// then deleting necessary tags.
// Will use the document provided to create a new document using:
// Document.implementation.createHTMLDocument()
// @param msgBody [String] the html content of the email
// @param dom [Document] a document object or equivalent implementation.
// Must respond to `DOMImplementation.createHTMLDocument()`.
// @see https://developer.mozilla.org/en-US/docs/Web/API/DOMImplementation/createHTMLDocument
exports.extractFromHtml = function(msgBody, dom) {
var checkpoint, crlfReplaced, emailDocument, emailDocumentCopy, haveCutQuotations, i, index, k, l, len, len1, line, lineCheckpoints, lines, m, markers, matches, numberOfCheckpoints, plainTextMsg, quotationCheckpoints, ref, ref1, ref2, ref3, returnFlags;
var checkpoint, crlfReplaced, emailDocument, emailDocumentCopy, haveCutQuotations, i, index, k, l, len, len1, line, lineCheckpoints, lines, m, markers, matches, numberOfCheckpoints, plainTextMsg, quotationCheckpoints, ref, ref1, ref2, returnFlags;
if (dom == null) {

@@ -56,6 +90,12 @@ console.error("No dom provided to parse html.");

}
ref = _CRLF_to_LF(msgBody), msgBody = ref[0], crlfReplaced = ref[1];
[msgBody, crlfReplaced] = _CRLF_to_LF(msgBody);
emailDocument = htmlPlaner.createEmailDocument(msgBody, dom);
// TODO: this check does not handle cases of emails between various email providers well because
// it will find whichever splitter comes first in this list, not necessarily the top-most and stop
// checking for others. Possible solution is to use something like compareByDomPosition from htmlPlaner
// to find the earliest splitter in the DOM.
haveCutQuotations = htmlPlaner.cutGmailQuote(emailDocument) || htmlPlaner.cutBlockQuote(emailDocument) || htmlPlaner.cutMicrosoftQuote(emailDocument) || htmlPlaner.cutById(emailDocument) || htmlPlaner.cutFromBlock(emailDocument);
// Create unaltered copy of email document
emailDocumentCopy = htmlPlaner.createEmailDocument(emailDocument.documentElement.outerHTML, dom);
// Add checkpoints to html document
numberOfCheckpoints = htmlPlaner.addCheckpoints(emailDocument.body, 0);

@@ -65,2 +105,3 @@ quotationCheckpoints = Array.apply(null, Array(numberOfCheckpoints)).map(function() {

});
// Get plain text version to put through plain text algorithm
htmlPlaner.replaceBreakTagsWithLineFeeds(emailDocument);

@@ -73,2 +114,3 @@ plainTextMsg = emailDocument.body.textContent;

}
// Collect checkpoints for each line
lineCheckpoints = new Array(lines.length);

@@ -82,2 +124,3 @@ for (index = k = 0, len = lines.length; k < len; index = ++k) {

}
// Remove checkpoints from lines to pass through plain text algorithm
lines = lines.map(function(line) {

@@ -91,17 +134,21 @@ return line.replace(htmlPlaner.CHECKPOINT_PATTERN, '');

if (haveCutQuotations) {
// If we cut a quotation element out of the html, return the html output of the copied document.
return _restore_CRLF(emailDocumentCopy.documentElement.outerHTML, crlfReplaced);
} else {
// There was nothing to remove, return original message.
return msgBody;
}
}
for (i = l = ref1 = returnFlags.firstLine, ref2 = returnFlags.lastLine; ref1 <= ref2 ? l <= ref2 : l >= ref2; i = ref1 <= ref2 ? ++l : --l) {
// Set quotationCheckpoints to true for checkpoints on lines that were removed
for (i = l = ref = returnFlags.firstLine, ref1 = returnFlags.lastLine; (ref <= ref1 ? l <= ref1 : l >= ref1); i = ref <= ref1 ? ++l : --l) {
if (!lineCheckpoints[i]) {
continue;
}
ref3 = lineCheckpoints[i];
for (m = 0, len1 = ref3.length; m < len1; m++) {
checkpoint = ref3[m];
ref2 = lineCheckpoints[i];
for (m = 0, len1 = ref2.length; m < len1; m++) {
checkpoint = ref2[m];
quotationCheckpoints[checkpoint] = true;
}
}
// Remove the element that have been identified as part of the quoted message
htmlPlaner.deleteQuotationTags(emailDocumentCopy.body, 0, quotationCheckpoints);

@@ -111,2 +158,14 @@ return emailDocumentCopy.documentElement.outerHTML;

// Mark message lines with markers to distinguish quotation lines.
// Markers:
// * e - empty line
// * f - Forwarded message line, see REGEXES.FWD
// * m - line that starts with quotation marker '>'
// * s - splitter line
// * t - presumably lines from the last message in the conversation
// $> markMessageLines(['answer', 'From: foo@bar.com', '', '> question'])
// 'tsem'
exports.markMessageLines = function(lines) {

@@ -118,12 +177,13 @@ var i, j, k, markers, ref, splitter, splitterLines;

if (lines[i].trim() === '') {
markers[i] = 'e';
markers[i] = 'e'; // empty line
} else if (REGEXES.QUOT_PATTERN.test(lines[i])) {
markers[i] = 'm';
markers[i] = 'm'; // line with quotation marker
} else if (REGEXES.FWD.test(lines[i])) {
markers[i] = 'f';
markers[i] = 'f'; // ---- Forwarded message ----
} else {
splitter = isSplitter(lines.slice(i, i + SPLITTER_MAX_LINES).join("\n"));
if (splitter) {
// splitter[0] is the entire match
splitterLines = splitter[0].split("\n");
for (j = k = 0, ref = splitterLines.length; 0 <= ref ? k <= ref : k >= ref; j = 0 <= ref ? ++k : --k) {
for (j = k = 0, ref = splitterLines.length; (0 <= ref ? k <= ref : k >= ref); j = 0 <= ref ? ++k : --k) {
markers[i + j] = 's';

@@ -141,2 +201,3 @@ }

// Check the line for each splitter regex.
isSplitter = function(line) {

@@ -158,10 +219,18 @@ var k, len, matchArray, pattern, ref;

exports.processMarkedLines = function(lines, markers, returnFlags) {
// Run regexes against message's marked lines to strip quotations.
// Return only last message lines.
// $> processMarkedLines(['Hello', 'From: foo@bar.com', '', '> Hi'], 'tsem'])
// ['Hello']
// Will also modify the provided returnFlags object and set the following properties:
// returnFlags = { wereLinesDeleted: (true|false), firstLine: (Number), lastLine: (Number) }
// @see setReturnFlags
exports.processMarkedLines = function(lines, markers, returnFlags = {}) {
var inlineMatchRegex, inlineReplyIndex, inlineReplyMatch, isInlineReplyLink, quotationEnd, quotationMatch;
if (returnFlags == null) {
returnFlags = {};
}
// If there are no splitters there should be no markers
if (markers.indexOf('s') < 0 && !/(me*){3}/.test(markers)) {
markers = markers.replace(/m/g, 't');
}
// If the message is a forward do nothing.
if (/^[te]*f/.test(markers)) {

@@ -171,2 +240,3 @@ setReturnFlags(returnFlags, false, -1, -1);

}
// Find inline replies (tm's following the first m in markers string)
inlineMatchRegex = new RegExp('m(?=e*((?:t+e*)+)m)', 'g');

@@ -184,2 +254,3 @@ while (inlineReplyMatch = inlineMatchRegex.exec(lines)) {

}
// Cut out text lines coming after splitter if there are no markers there
quotationMatch = new RegExp('(se*)+((t|f)+e*)+', 'g').exec(markers);

@@ -190,2 +261,3 @@ if (quotationMatch) {

}
// Handle the case with markers
quotationMatch = REGEXES.QUOTATION.exec(markers) || REGEXES.EMPTY_QUOTATION.exec(markers);

@@ -207,19 +279,29 @@ if (quotationMatch) {

preprocess = function(msgBody, delimiter, contentType) {
if (contentType == null) {
contentType = 'text/plain';
}
// Prepares msgBody for being stripped.
// Replaces link brackets so that they couldn't be taken for quotation marker.
// Splits line in two if splitter pattern preceded by some text on the same
// line (done only for 'On <date> <person> wrote:' pattern).
preprocess = function(msgBody, delimiter, contentType = 'text/plain') {
// Normalize links i.e. replace '<', '>' wrapping the link with some symbols
// so that '>' closing the link couldn't be mistakenly taken for quotation
// marker.
// REGEXES.LINK has 1 captured group
msgBody = msgBody.replace(REGEXES.LINK, function(entireMatch, groupMatch1, matchIndex) {
var newLineIndex;
// Look for closest newline character
newLineIndex = msgBody.lastIndexOf("\n", matchIndex);
// If the new current line starts with a '>' quotation marker, don't mess with the link
if (newLineIndex > 0 && msgBody[newLineIndex + 1] === '>') {
return entireMatch;
} else {
return "@@" + groupMatch1 + "@@";
return `@@${groupMatch1}@@`;
}
});
if (contentType === 'text/plain' && msgBody.length < MAX_LINE_LENGTH) {
// ON_DATE_SMB_WROTE has 4 captured groups
msgBody = msgBody.replace(REGEXES.ON_DATE_SMB_WROTE, function(entireMatch, groupMatch1, groupMatch2, groupMatch3, groupMatch4, matchIndex) {
if (matchIndex && msgBody[matchIndex - 1] !== "\n") {
return "" + delimiter + entireMatch;
return `${delimiter}${entireMatch}`;
} else {

@@ -233,2 +315,4 @@ return entireMatch;

// Make up for changes done at preprocessing message.
// Replace link brackets back to '<' and '>'.
postprocess = function(msgBody) {

@@ -265,6 +349,3 @@ return msgBody.replace(REGEXES.NORMALIZED_LINK, '<$1>').trim();

_restore_CRLF = function(msgBody, replaced) {
if (replaced == null) {
replaced = true;
}
_restore_CRLF = function(msgBody, replaced = true) {
if (replaced) {

@@ -271,0 +352,0 @@ return msgBody.replace(new RegExp('\n', 'g'), '\r\n');

@@ -1,2 +0,2 @@

// Generated by CoffeeScript 1.12.7
// Generated by CoffeeScript 2.5.1
(function() {

@@ -7,4 +7,6 @@ exports.DELIMITER = new RegExp('\r?\n');

// On {date}, {somebody} wrote:
exports.ON_DATE_SMB_WROTE = new RegExp("(-*[>]?[ ]?(On|Le|W dniu|Op|Am|P\xe5|Den)[ ].*(,|u\u017cytkownik)(.*\n){0,2}.*(wrote|sent|a \xe9crit|napisa\u0142|schreef|verzond|geschreven|schrieb|skrev):?-*)");
// On {date} wrote {somebody}:
exports.ON_DATE_WROTE_SMB = new RegExp('(-*[>]?[ ]?(Op|Am)[ ].*(.*\n){0,2}.*(schreef|verzond|geschreven|schrieb)[ ]*.*:)');

@@ -11,0 +13,0 @@

{
"name": "@salesflare/planer",
"version": "1.1.3",
"version": "2.0.0",
"description": "Remove reply quotations from emails",

@@ -10,3 +10,3 @@ "main": "lib/planer.js",

"scripts": {
"test": "mocha test/",
"test": "mocha --require coffeescript/register --recursive --reporter spec test/**",
"compile": "coffee -o lib -c src"

@@ -31,7 +31,7 @@ },

"devDependencies": {
"chai": "^3.4.1",
"coffee-script": "^1.10.0",
"jsdom": "^11.6.0",
"mocha": "^2.3.4"
"chai": "^4.2.0",
"coffeescript": "^2.5.1",
"jsdom": "^16.4.0",
"mocha": "^8.1.3"
}
}
# planer
Remove reply quotations from emails.
At [lever](https://github.com/lever) we are into simple machines.
At [lever](https://github.com/lever) we are into simple machines.
A planer removes some material to flatten out a rough surface, which seemed appropriate for a module that smooths out an email to extract the actual message.

@@ -10,3 +11,4 @@

# Installation
## Installation
Use npm to install planer (add `-g` if you would like it to be global):

@@ -16,10 +18,11 @@

# Usage
## Usage
_Important_: planer accepts an injected [Document](https://developer.mozilla.org/en-US/docs/Web/API/Document) object to perform html parsing.
You can use `window.document` in a browser, or something akin to `jsdom` on the server.
You can use `window.document` in a browser, or something akin to `jsdom` on the server.
We use `jsdom` in our test suite.
To extract the message from a plain text email:
```
```js
planer = require('planer');

@@ -33,3 +36,4 @@

To extract the message from an html email:
```
```js
planer = require('planer');

@@ -42,3 +46,3 @@

# Contributing
## Contributing

@@ -51,5 +55,4 @@ Contributions are of course encouraged.

## MIT Licence
# MIT Licence
Copyright (c) 2015 Leighton Wallace

@@ -56,0 +59,0 @@

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc