Is Streak CRM for Gmail safe?
Streak is medium risk. When email tracking is enabled for a sent Gmail message, Streak adds a zero-size image to the outgoing HTML. The URL points to mailfoogae.appspot.com/t and includes the sender email base64-encoded with an 'a' prefix, plus a message GUID.…
Who publishes itStreak - 3 other listings from the same operator, none carrying a finding
Streak - 3 other listings from the same operator, none carrying a finding
What this publisher told the store about itself, and the other listings that told it the same thing.
Same store account
3 other listings published from this account, 93k+ users between them, none of them carrying a finding.
Shared hosts - 2 hostnames
Hostnames hardcoded in this extension that few other listings call. That can mean one operator behind both, and it can equally mean a small shared vendor, so it is context rather than a conclusion. Hosts that many listings call are left out: they are services, not connections.
AI-generated. Findings may contain errors. Those marked Verified have been manually reviewed.
Publishers can request a review.
Findings
Tracking Pixel URL Encodes the Sender Email
When email tracking is enabled for a sent Gmail message, Streak adds a zero-size image to the outgoing HTML.
The URL points to mailfoogae.appspot.com/t and includes the sender email base64-encoded with an 'a' prefix, plus a message GUID.
You send a Gmail message with Streak tracking enabled.
The extension adds a zero-size tracking image whose URL includes your encoded sender email and a message GUID.
| Field | Value | Why it matters | |
|---|---|---|---|
Sender email | aY293YW5Ac3RyZWFrLmNvbQ%3D%3D | Identifies you as the Streak account that sent the tracked email. | |
Tracking type | zerocontent | Marks the image request as the zero-content tracking pixel for the message. | |
Message GUID | 85fe5cbf-f8b4-468b-84fa-e47303991f07 | Lets the service associate the image request with a specific tracked email. |
The sender field is URL-encoded base64 with a leading 'a' marker.
cowan@streak.com
The compose hook inserts the pixel and the generator builds the URL
composeView.registerRequestModifier(async composeParams => {
wasRequestModifierCallbackCalled = true;
isPlainText = composeWindowViewController.isPlainText();
shouldTrack = this.#shouldTrack();
// Check if the sdk given isPlainText agrees with our isPlainText
if (composeParams.isPlainText != null && composeParams.isPlainText !== isPlainText) {
track('sdk isPlainText does not agree with our isPlainText', {
draftSaved
});
}
if (shouldTrack && isPlainText && !shouldShowPlainTextModal) {
track('plain text check was wrong', {
draftSaved
});
}
if (shouldTrack && !isPlainText) {
// get encodedUrl from server and update composeParams.body for all links that should be tracked
const trackingLinks = this.#getLinksForTracking(composeView);
const encodedUrlFetchPromises = trackingLinks.map(link => {
const linkHref = link.getAttribute('href');
return getEncodedUrl(linkHref);
});
try {
const encodedHrefResponses = await Promise.all(encodedUrlFetchPromises);
// not track all links if we detect one link isn't safe
if (!encodedHrefResponses.some(response => response.threatStatus === 'THREAT_FOUND')) {
trackingLinks.forEach((link, index) => {
const value = encodedHrefResponses[index];
if (value?.linkHref === link.getAttribute('href') && value?.linkHref !== value?.encodedHref && (0,_linkTrackingValidation__WEBPACK_IMPORTED_MODULE_20__/* .isLinkCandidateToTrack */ .s2)(link)) {
composeParams.body = (0,_linkTrackingReplacementHelpers__WEBPACK_IMPORTED_MODULE_21__/* .replaceLink */ .t)(link, value.encodedHref, composeParams.body);
weakMapOfUrlsToTrackedUrls.set(link, value.encodedHref);
}
});
// clean out the tracking hspace
composeParams.body = (0,_linkTrackingReplacementHelpers__WEBPACK_IMPORTED_MODULE_21__/* .removeHspaceTrackingInfo */ .U)(composeParams.body);
}
} catch (error) {
// Too long regexes trigger new sentry errors for every error instance otherwise.
if (error instanceof SyntaxError) {
(0,_lib_log_error__WEBPACK_IMPORTED_MODULE_7__/* ["default"] */ .Ay)({
details: {
message: error.message
},
err: new Error(`Compose regex ${error.name}`, {
cause: error
}),
level: 'warning'
});
} else {
(0,_lib_log_error__WEBPACK_IMPORTED_MODULE_7__/* ["default"] */ .Ay)({
err: error,
level: 'error'
});
}
}
const {
guid,
body
} = _services_pixelTracking_pixelTrackingHTMLGenerator__WEBPACK_IMPORTED_MODULE_14__["default"].injectTrackingHTML(composeParams.body);
trackedLinkData.guid = guid;
const allRecipients = lodash__WEBPACK_IMPORTED_MODULE_0___default().uniqBy(composeView.getToRecipients().concat(composeView.getCcRecipients()).concat(composeView.getBccRecipients())
// TODO can we make use of contacts without names? We're
// defensively filtering them out for now.
.filter(contact => contact.name != null), contact => contact.emailAddress);
trackedLinkData.names = allRecipients.map(contact => contact.name);
trackedLinkData.emails = allRecipients.map(contact => contact.emailAddress);
trackedLinkData.subject = composeView.getSubject();
trackedLinkData.snippetKeyList = this.#getUsedSnippetKeys();
composeParams.body = body;
}
return composeParams;
});class PixelTrackingHTMLGenerator {
/**
* Returns the GUID for the image, returns cached version if it.
* exists Otherwise it's generated from a very simple algorithm.
*
* @return {String}, GUID in string version.
*/
getGuid() {
// GUID doens't exist yet, so we generate a unique one from
// a very simple pattern:
return (0,uuid__WEBPACK_IMPORTED_MODULE_2__/* ["default"] */ .A)();
}
injectTrackingHTML(body) {
const guid = this.getGuid();
const trackingHTML = this.getTrackingHTML(guid);
const jqDiv = jquery__WEBPACK_IMPORTED_MODULE_1___default()(document.createElement('div'));
jqDiv[0].innerHTML = body;
jqDiv.append(trackingHTML);
return {
guid,
body: jqDiv.html()
};
}
getTrackingHTML(guid) {
if (!guid) {
guid = this.getGuid();
}
return ['<div hspace="streak-pt-mark" style="max-height:1px;">', this._getImage(guid), this._getSearchMarker(), '</div>'].join('');
}
obfuscateEmailAddress(email) {
return 'a' + buffer___WEBPACK_IMPORTED_MODULE_3__/* .Buffer */ .hp.from(email).toString('base64');
}
getObfuscatedEmailAddress() {
return this.obfuscateEmailAddress((0,_streakyc_common_services_data_user__WEBPACK_IMPORTED_MODULE_0__/* .getUser */ .w)().getEmail());
}
getEncodedEmailAddress() {
return encodeURIComponent(this.getObfuscatedEmailAddress());
}
/**
* Returns the full image string.
*
* @return {String}, string version of the image with GUID.
*/
_getImage(guid) {
return '<img alt="" style="width:0px;max-height:0px;overflow:hidden" src=\'' + "https://mailfoogae.appspot.com" + '/t?sender=' + this.getEncodedEmailAddress() + '&type=zerocontent&guid=' + guid + "'/>";
}
_getSearchMarker() {
return '<font color="#ffffff" size="1">' + _core_routing__WEBPACK_IMPORTED_MODULE_4__/* .StreakSearchQuery */ .jZ.PIXEL_TRACKER_DOT + '</font>';
}
}
const pixelTrackingHTMLGeneratorSingleton = new PixelTrackingHTMLGenerator();
/* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = (pixelTrackingHTMLGeneratorSingleton);- mailfoogae.appspot.com
Receives the email-open tracking image request generated by Streak CRM for Gmail.
LinkedIn Profile Details Sent to Streak Contact API
After the LinkedIn permission is granted and you use Streak's LinkedIn integration, the content script reads the profile name, derives the handle from the URL path, and submits both to Streak's contact API for creation or lookup.
You use Streak's LinkedIn integration on a LinkedIn profile page.
The content script reads the visible profile name, prepares contact fields, and sends them to Streak.
| Field | Value | Why it matters | |
|---|---|---|---|
Profile full name | Ada Lovelace (illustrative) | Identifies the person whose LinkedIn profile you are viewing. | |
Given name | Ada (illustrative) | Adds the first-name portion of the viewed profile to the contact record. | |
Family name | Lovelace (illustrative) | Adds the last-name portion of the viewed profile to the contact record. | |
LinkedIn handle | in/ada-lovelace (illustrative) | Links the contact record to the LinkedIn profile you opened. |
The LinkedIn integration registers, reads the profile, and posts contact fields
const LINKEDIN_ORIGINS = '*://*.linkedin.com/*';
const LINKEDIN_PERMISSION = {
origins: [LINKEDIN_ORIGINS],
};
/**
* This array contains content scripts that are registered conditionally based on permissions granted by the user.
*/
const OPTIONAL_CONTENT_SCRIPTS = [
{
id: 'linkedin-content-script',
permission: LINKEDIN_PERMISSION,
registerOptions: {
// Payload should follow the `chrome.scripting.registerContentScripts()` API
matches: [LINKEDIN_ORIGINS],
js: ['app-mv3.js'],
runAt: 'document_start',
},
},
];function getProfileUserPanelElement() {
// Old LinkedIn UI selectors
const oldPanel = document.querySelector('#profile-content section.artdeco-card[data-member-id], [data-view-name="profile-top-card-verified-badge"]');
if (oldPanel) {
return oldPanel;
}
// Current LinkedIn UI: profile name heading is inside a section in main
const nameHeading = document.querySelector('main section h1') ?? document.querySelector('main section h2');
if (nameHeading) {
return nameHeading.closest('section');
}
return null;
}
function getProfileFullNameElement(containerElement) {
const container = containerElement ?? getProfileUserPanelElement();
if (!container) return null;
return container.querySelector('h1, h2, p');
}
;// ./extensions/common/js/lib/linkedinSDK/internal/dom-driver/utils/extract-profile-user-details.ts
function extractProfileUserDetailsFromPanel(panelEl) {
const details = {};
if (!panelEl) return details;
const element = getProfileFullNameElement(panelEl);
if (!element) return details;
const name = element.textContent?.trim();
if (name) {
details.fullName = name;
}
return details;
}
function getProfileFullNameElementWithText() {
const panelEl = getProfileUserPanelElement();
if (!panelEl) return null;
const fullNameElement = getProfileFullNameElement(panelEl);
if (!(fullNameElement instanceof HTMLElement)) return null;
const fullName = fullNameElement.textContent?.trim();
if (!fullName) return null;
return fullNameElement;
}
function getProfileUserDetails() {
return extractProfileUserDetailsFromPanel(getProfileUserPanelElement());
}const getContactCreatePayload = fullName => {
const nameParts = extractsNameParts(fullName);
return {
givenName: nameParts.givenName,
familyName: nameParts.familyName,
linkedinHandle: getCleanLinkedinHandle(window.location.pathname)
};
};
const getCleanLinkedinHandle = pathname => {
// Extract just the username part, ignoring additional path segments
const match = pathname.match(/^\/?in\/([^/]+)/);
return match ? `in/${match[1]}` : null;
};async function fetchApi_createContact(contactPayload) {
const res = await linkedin_apiRequester.post(`teams/${contactPayload.teamKey}/contacts/`, {
json: contactPayload
}, null, {
prefix: '/api/v2/',
templateUrl: 'teams/KEY/contacts',
retry: false
}).getPromise();
return res;
} const createTeamContact = async teamKey => {
try {
const details = getProfileUserDetails();
if (!details.fullName) {
throw new Error('LinkedIn profile missing full name for team contact creation');
}
const payload = getContactCreatePayload(details.fullName);
if (!payload.linkedinHandle) {
throw new Error('Missing LinkedIn handle for contact creation');
}
await createContact(teamKey, {
givenName: payload.givenName,
familyName: payload.familyName,
linkedinHandle: payload.linkedinHandle
});
track('profileDetailsButton.createTeamContact', {
teamKey
});
setSentToStreak(true);
refetchData();
} catch (error) {
logError({
err: new Error('LinkedIn team contact creation failed', {
cause: error
}),
details: {
teamKey
}
});
}
};- api.streak.com
Receives the LinkedIn-derived contact fields for Streak contact creation or lookup.