Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,8 @@ A browser extension for multi-account Gmail™ notifications. Get a badge counte

## Google account warning

When adding a Gmail account you will see a Google warning: **"This app hasn't been verified by Google"**. This is expected and safe to dismiss — it appears because Geething is an independent open-source project and Google's verification programme for apps that access Gmail requires a paid third-party security audit. The extension is not yet enrolled in that programme.
When adding a Gmail account you will see a Google warning: **"This app hasn't been verified by Google"**. This is expected and safe to dismiss — it appears because Geething is an independent open-source project and Google's verification programme for apps that access Gmail requires a paid third-party security audit. **The extension hasn't been verified, because it costs hundreds of dollars to do so, even for open-source projects.**
This all is an expected issue, because this extension uses `gmail.modify` permission scope, which allows modifying your Gmail data (marking messages read, archiving, etc.) — and Google is extra cautious about apps that request such permissions.

You can verify for yourself that the extension is safe:
- The full source code is in this repository — nothing is hidden or obfuscated.
Expand Down
4 changes: 1 addition & 3 deletions src/background/accounts.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ACCOUNT_COLORS } from '../shared/constants.js';
import { ACCOUNT_COLORS, TOKEN_SKEW_MS } from '../shared/constants.js';
import {
clearAccountData,
getAccounts as readAccounts,
Expand All @@ -8,8 +8,6 @@ import {
} from '../shared/storage.js';
import { launchOAuth, refreshAccessToken, revokeToken } from './auth.js';

const TOKEN_SKEW_MS = 60 * 1000; // Refresh 60s before expiry.

export class AuthError extends Error {
constructor(message) {
super(message);
Expand Down
72 changes: 72 additions & 0 deletions src/background/gmail-actions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { gmailFetch } from './gmail-api.js';

export async function markAsRead(accessToken, messageId) {
return gmailFetch(accessToken, `/users/me/messages/${messageId}/modify`, {
method: 'POST',
body: { removeLabelIds: ['UNREAD'] },
});
}

export async function markAsUnread(accessToken, messageId) {
return gmailFetch(accessToken, `/users/me/messages/${messageId}/modify`, {
method: 'POST',
body: { addLabelIds: ['UNREAD'] },
});
}

export async function moveToTrash(accessToken, messageId) {
return gmailFetch(accessToken, `/users/me/messages/${messageId}/trash`, { method: 'POST' });
}

export async function markAsSpam(accessToken, messageId) {
return gmailFetch(accessToken, `/users/me/messages/${messageId}/modify`, {
method: 'POST',
body: { addLabelIds: ['SPAM'], removeLabelIds: ['INBOX'] },
});
}

export async function archiveMessage(accessToken, messageId) {
return gmailFetch(accessToken, `/users/me/messages/${messageId}/modify`, {
method: 'POST',
body: { removeLabelIds: ['INBOX'] },
});
}

export async function starMessage(accessToken, messageId) {
return gmailFetch(accessToken, `/users/me/messages/${messageId}/modify`, {
method: 'POST',
body: { addLabelIds: ['STARRED'] },
});
}

export async function unstarMessage(accessToken, messageId) {
return gmailFetch(accessToken, `/users/me/messages/${messageId}/modify`, {
method: 'POST',
body: { removeLabelIds: ['STARRED'] },
});
}

export async function archiveThread(accessToken, threadId) {
return gmailFetch(accessToken, `/users/me/threads/${threadId}/modify`, {
method: 'POST',
body: { removeLabelIds: ['INBOX'] },
});
}

export async function trashThread(accessToken, threadId) {
return gmailFetch(accessToken, `/users/me/threads/${threadId}/trash`, { method: 'POST' });
}

export async function spamThread(accessToken, threadId) {
return gmailFetch(accessToken, `/users/me/threads/${threadId}/modify`, {
method: 'POST',
body: { addLabelIds: ['SPAM'], removeLabelIds: ['INBOX'] },
});
}

export async function markThreadRead(accessToken, threadId) {
return gmailFetch(accessToken, `/users/me/threads/${threadId}/modify`, {
method: 'POST',
body: { removeLabelIds: ['UNREAD'] },
});
}
73 changes: 1 addition & 72 deletions src/background/gmail-api.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ class HttpError extends Error {

export { HttpError };

async function gmailFetch(accessToken, path, { method = 'GET', body, query } = {}) {
export async function gmailFetch(accessToken, path, { method = 'GET', body, query } = {}) {
const url = new URL(`${GMAIL_API_BASE}${path}`);
if (query) {
for (const [key, value] of Object.entries(query)) {
Expand Down Expand Up @@ -80,77 +80,6 @@ export async function fetchMessageDetail(accessToken, messageId) {
return parseMessage(data, { includeBody: true });
}

export async function markAsRead(accessToken, messageId) {
return gmailFetch(accessToken, `/users/me/messages/${messageId}/modify`, {
method: 'POST',
body: { removeLabelIds: ['UNREAD'] },
});
}

export async function markAsUnread(accessToken, messageId) {
return gmailFetch(accessToken, `/users/me/messages/${messageId}/modify`, {
method: 'POST',
body: { addLabelIds: ['UNREAD'] },
});
}

export async function moveToTrash(accessToken, messageId) {
return gmailFetch(accessToken, `/users/me/messages/${messageId}/trash`, { method: 'POST' });
}

export async function markAsSpam(accessToken, messageId) {
return gmailFetch(accessToken, `/users/me/messages/${messageId}/modify`, {
method: 'POST',
body: { addLabelIds: ['SPAM'], removeLabelIds: ['INBOX'] },
});
}

export async function archiveMessage(accessToken, messageId) {
return gmailFetch(accessToken, `/users/me/messages/${messageId}/modify`, {
method: 'POST',
body: { removeLabelIds: ['INBOX'] },
});
}

export async function starMessage(accessToken, messageId) {
return gmailFetch(accessToken, `/users/me/messages/${messageId}/modify`, {
method: 'POST',
body: { addLabelIds: ['STARRED'] },
});
}

export async function unstarMessage(accessToken, messageId) {
return gmailFetch(accessToken, `/users/me/messages/${messageId}/modify`, {
method: 'POST',
body: { removeLabelIds: ['STARRED'] },
});
}

export async function archiveThread(accessToken, threadId) {
return gmailFetch(accessToken, `/users/me/threads/${threadId}/modify`, {
method: 'POST',
body: { removeLabelIds: ['INBOX'] },
});
}

export async function trashThread(accessToken, threadId) {
return gmailFetch(accessToken, `/users/me/threads/${threadId}/trash`, { method: 'POST' });
}

export async function spamThread(accessToken, threadId) {
return gmailFetch(accessToken, `/users/me/threads/${threadId}/modify`, {
method: 'POST',
body: { addLabelIds: ['SPAM'], removeLabelIds: ['INBOX'] },
});
}

export async function markThreadRead(accessToken, threadId) {
return gmailFetch(accessToken, `/users/me/threads/${threadId}/modify`, {
method: 'POST',
body: { removeLabelIds: ['UNREAD'] },
});
}

export async function getProfile(accessToken) {
return gmailFetch(accessToken, '/users/me/profile');
}
Expand Down
31 changes: 10 additions & 21 deletions src/background/service-worker.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { ALARM_NAMES, MAX_FETCH_MESSAGES } from '../shared/constants.js';
import { ALARM_NAMES, MAX_FETCH_MESSAGES, POLL_GUARD_TIMEOUT_MS } from '../shared/constants.js';
import { isGloballyMuted } from '../shared/mute.js';
import { DEV_MESSAGE_DETAILS, seedDevData } from './dev-seed.js';
import {
clearCustomSound,
Expand Down Expand Up @@ -33,20 +34,22 @@ import {
import {
archiveMessage,
archiveThread,
spamThread,
fetchAttachment,
fetchLabels,
fetchMessageDetail,
fetchMessageMetadata,
fetchUnreadMessageIds,
markAsRead,
markAsSpam,
markAsUnread,
markThreadRead,
moveToTrash,
spamThread,
starMessage,
trashThread,
unstarMessage,
} from './gmail-actions.js';
import {
fetchAttachment,
fetchLabels,
fetchMessageDetail,
fetchMessageMetadata,
fetchUnreadMessageIds,
} from './gmail-api.js';

const api = typeof browser !== 'undefined' ? browser : globalThis.chrome;
Expand All @@ -57,7 +60,6 @@ const accountState = new Map();
// Guard against concurrent polls that would cause duplicate notifications.
// A safety timer releases the lock if a poll hangs beyond POLL_GUARD_TIMEOUT_MS
// so a single stuck fetch cannot freeze the extension indefinitely.
const POLL_GUARD_TIMEOUT_MS = 5 * 60 * 1000;
let pollRunning = false;
let pollGuardTimer = null;

Expand All @@ -68,19 +70,6 @@ function setAccountState(accountId, patch) {
return next;
}

function isGloballyMuted(mute) {
if (!mute) {
return false;
}
if (mute.muteUntil === -1) {
return true;
}
if (!mute.muteUntil) {
return false;
}
return Date.now() < mute.muteUntil;
}

// Updates the badge to the total unread count, or to '!' (amber) if any
// account needs re-authorization and there are no unread messages.
// When globally muted, the muted badge takes priority over everything.
Expand Down
Loading
Loading