importScripts("config.js");
const browserAPI = typeof browser !== "undefined" ? browser : chrome;
let accessToken = null;
let tokenStoredAt = null;
const MAX_AGE = 8 * 60 * 60 * 1000;
/**
* It authenticates the user using OAuth2
* @returns the authentification token
*/
async function getAccessToken() {
// Check memory with age
if (accessToken && tokenStoredAt && Date.now() - tokenStoredAt < MAX_AGE) {
console.log("Token found in memory, still valid");
return accessToken;
}
// Check storage with age
const stored = await browserAPI.storage.local.get([
"access_token",
"token_stored_at",
]);
if (
stored.access_token &&
stored.token_stored_at &&
Date.now() - stored.token_stored_at < MAX_AGE
) {
accessToken = stored.access_token;
tokenStoredAt = stored.token_stored_at;
console.log("Token found in storage, still valid");
return accessToken;
}
// Token missing or expired
accessToken = null;
tokenStoredAt = null;
await browserAPI.storage.local.remove(["access_token", "token_stored_at"]);
console.log("Token expired or missing, launching auth flow...");
const redirectUri = browserAPI.identity.getRedirectURL();
const scopes = CONFIG.scopes.join(" ");
const authUrl =
"https://accounts.google.com/o/oauth2/v2/auth" +
"?client_id=" +
encodeURIComponent(CONFIG.client_id) +
"&response_type=token" +
"&redirect_uri=" +
encodeURIComponent(redirectUri) +
"&scope=" +
encodeURIComponent(scopes) +
"&prompt=login" +
"&max_age=28800";
return new Promise((resolve, reject) => {
browserAPI.identity.launchWebAuthFlow(
{ url: authUrl, interactive: true },
async (redirectedTo) => {
if (browserAPI.runtime.lastError) {
reject(browserAPI.runtime.lastError);
return;
}
const hash = new URL(redirectedTo).hash.substring(1);
const params = new URLSearchParams(hash);
const token = params.get("access_token");
if (!token) {
reject(new Error("Access token not found"));
return;
}
const now = Date.now();
accessToken = token;
tokenStoredAt = now;
await browserAPI.storage.local.set({
access_token: token,
token_stored_at: now,
});
console.log("Token stored");
resolve(accessToken);
},
);
});
}
async function handlerToken() {
try {
const token = await getAccessToken();
console.log(token);
return token;
} catch (error) {
browserAPI.notifications.create(`auth_denied_${Date.now()}`, {
type: "basic",
iconUrl: browserAPI.runtime.getURL("icons/error-48.png"),
title: "MailCategoryManager",
message: "Access denied. Please try again.",
});
return null;
}
}
async function deleteLabel(labelName) {
const token = await handlerToken();
if (!token) return "Authentication failed.";
const listResponse = await fetch(
"https://www.googleapis.com/gmail/v1/users/me/labels",
{ headers: { Authorization: `Bearer ${token}` } },
);
if (!listResponse.ok) {
const err = await listResponse.json();
return `Error fetching labels: ${err.error?.message || listResponse.statusText}`;
}
const listData = await listResponse.json();
const labels = listData.labels || [];
const toDelete = labels.filter((l) => {
const normalizedLabel = l.name.replace(/^\//, "");
const normalizedInput = labelName.replace(/^\//, "");
// match if label contains the input anywhere in the path
return (
normalizedLabel === normalizedInput ||
normalizedLabel.includes(normalizedInput)
);
});
console.log(toDelete);
if (toDelete.length === 0) {
browserAPI.notifications.create(
`label_error_${Date.now()}`,
{
type: "basic",
iconUrl: browserAPI.runtime.getURL("icons/error-48.png"),
title: "GmailCategoryManager Error",
message: `Label '${labelName}' not found!`,
},
(id) => {
console.log("Notification created:", id);
},
);
return `Label '${labelName}' not found.`;
}
const results = await Promise.all(
toDelete.map(async (label) => {
const deleteResponse = await fetch(
`https://www.googleapis.com/gmail/v1/users/me/labels/${label.id}`,
{
method: "DELETE",
headers: { Authorization: `Bearer ${token}` },
},
);
if (!deleteResponse.ok) {
const errorData = await deleteResponse.json();
return `Failed to delete '${label.name}': ${errorData.error?.message}`;
}
return `'${label.name}' deleted`;
}),
);
console.log("Deleted labels:", results);
browserAPI.notifications.create(`label_deleted_${Date.now()}`, {
type: "basic",
iconUrl: browserAPI.runtime.getURL("icons/success-48.png"),
title: "GmailCategoryManager",
message: `Deleted ${toDelete.length} label(s) under '${labelName}'`,
});
return results.join(", ");
}
async function createLabel(labelName, color) {
const token = await handlerToken();
if (!token) return "Authentication failed.";
try {
const parts = labelName.split("/");
let createdLabel;
for (let i = 0; i < parts.length; i++) {
const partialPath = parts.slice(0, i + 1).join("/");
const label = {
name: partialPath,
labelListVisibility: "labelShow",
messageListVisibility: "show",
color: {
backgroundColor: color,
textColor: "#000000",
},
};
const response = await fetch(
"https://www.googleapis.com/gmail/v1/users/me/labels",
{
method: "POST",
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
body: JSON.stringify(label),
},
);
// 409 = label already exists, skip it
if (response.status === 409) continue;
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.error.message);
}
createdLabel = await response.json(); // ← saves last created label
}
console.log(
`Label created: ${createdLabel.name} with ID: ${createdLabel.id}`,
);
browserAPI.notifications.create(`label_created_${Date.now()}`, {
type: "basic",
iconUrl: browserAPI.runtime.getURL("icons/success-48.png"),
title: "GmailCategoryManager",
message: `Label created: ${createdLabel.name} with ID: ${createdLabel.id}`,
});
return `Label created: ${createdLabel.name} with ID: ${createdLabel.id}`;
} catch (error) {
console.error("Error creating label:", error.message);
browserAPI.notifications.create(`label_created_error_${Date.now()}`, {
type: "basic",
iconUrl: browserAPI.runtime.getURL("icons/error-48.png"),
title: "GmailCategoryManager Error",
message: `Error creating label: ${error.message}`,
});
return `Error creating label: ${error.message}`;
}
}
async function getLabelStats() {
const token = await handlerToken();
if (!token) throw new Error("Authentication failed.");
const response = await fetch(
"https://www.googleapis.com/gmail/v1/users/me/labels",
{ headers: { Authorization: `Bearer ${token}` } },
);
if (!response.ok) throw new Error("Failed to fetch labels");
const data = await response.json();
const labels = data.labels || [];
const userLabels = labels.filter((l) => l.type === "user");
return {
userCount: userLabels.length,
allLabels: userLabels.map((l) => ({
name: l.name,
color: l.color?.backgroundColor || null,
})),
};
}
browserAPI.runtime.onMessage.addListener((request, sender, sendResponse) => {
console.log("Received message:", request);
if (request.action === "notify_error") {
browserAPI.notifications.create(`label_error_${Date.now()}`, {
type: "basic",
iconUrl: browserAPI.runtime.getURL("icons/error-48.png"),
title: "GmailCategoryManager Error",
message: request.message || "An error occurred",
});
sendResponse({ success: true });
return true;
}
if (request.action === "createLabel") {
createLabel(request.labelName, request.color)
.then((result) => {
sendResponse({ success: true, result });
})
.catch((error) => {
browserAPI.notifications.create(`label_error_${Date.now()}`, {
type: "basic",
iconUrl: browserAPI.runtime.getURL("icons/error-48.png"),
title: "GmailCategoryManager Error",
message: error.message || "An error occurred",
});
sendResponse({ success: false, error: error.message });
});
return true;
}
if (request.action === "deleteLabel") {
deleteLabel(request.labelName)
.then((result) => {
sendResponse({ success: true, result });
})
.catch((error) => {
browserAPI.notifications.create(`label_error_${Date.now()}`, {
type: "basic",
iconUrl: browserAPI.runtime.getURL("icons/error-48.png"),
title: "GmailCategoryManager Error",
message: error.message || "An error occurred",
});
sendResponse({ success: false, error: error.message });
});
return true;
}
if (request.action === "getLabelStats") {
getLabelStats()
.then((stats) => {
sendResponse({ success: true, stats });
})
.catch((error) => {
sendResponse({ success: false, error: error.message });
});
return true;
}
});