-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathcontent.js
More file actions
247 lines (206 loc) · 8.61 KB
/
content.js
File metadata and controls
247 lines (206 loc) · 8.61 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
// Configuration constants
const CONFIG = {
// Debug mode - set to false in production
DEBUG: false,
// Time to wait after compose window is detected before interacting
// Increased to 1500ms to accommodate slower connections and ensure
// the window is fully loaded and interactive
COMPOSE_WINDOW_LOAD_DELAY: 1000, // 1 second, feel free to adjust if too slow/fast
// Time to wait after clicking signature button before looking for menu
// Set to 750ms to ensure menu is fully rendered and populated
SIGNATURE_MENU_DELAY: 500, // 1/2 second, feel free to adjust if too slow/fast
// Maximum number of processed window IDs to store in memory
// When this limit is reached, the oldest 50 IDs are removed
// 100 is sufficient for typical usage patterns while preventing memory leaks
MAX_PROCESSED_WINDOWS: 100,
// Signatures to exclude from random selection
// Add custom signatures to ignore by adding to this array
IGNORED_SIGNATURES: ['Manage signatures', 'No signature'],
// DOM selectors used to find relevant elements
// These are Gmail-specific class names and attributes
SELECTORS: {
// Class for the compose window container
COMPOSE_WINDOW: '.aoI',
// Signature button identifier
SIGNATURE_BUTTON: 'div[aria-label="Insert signature"]',
// Dropdown menu container
SIGNATURE_MENU: 'div[role="menu"]',
// Individual signature options in the menu
SIGNATURE_OPTIONS: 'div[role="menu"] div[role="menuitemcheckbox"]'
}
};
// Debug logging function
const debug = (...args) => {
if (CONFIG.DEBUG) {
console.log(...args);
}
};
debug('Gmail Signature Rotator: Content script loaded!');
// Function to simulate a more realistic click event sequence
function simulateClick(element) {
if (!element) {
console.error('Attempted to click null element');
return;
}
const events = ['mousedown', 'mouseup', 'click'];
const eventConfig = {
view: window,
bubbles: true,
cancelable: true,
buttons: 1
};
try {
events.forEach(eventType => {
element.dispatchEvent(new MouseEvent(eventType, eventConfig));
});
} catch (error) {
console.error('Error simulating click:', error);
}
}
// Keep track of processed compose windows to avoid duplicates
const processedWindows = new Set();
// Clean up old window IDs periodically
function cleanupProcessedWindows() {
if (processedWindows.size > CONFIG.MAX_PROCESSED_WINDOWS) {
const oldestEntries = Array.from(processedWindows)
.slice(0, CONFIG.MAX_PROCESSED_WINDOWS / 2);
oldestEntries.forEach(entry => processedWindows.delete(entry));
}
}
// Function to wait for an element to appear
async function waitForElement(selector, parent = document, timeout = 5000) {
const start = Date.now();
while (Date.now() - start < timeout) {
const element = parent.querySelector(selector);
if (element) return element;
await new Promise(resolve => setTimeout(resolve, 100));
}
console.warn(`Timeout waiting for element: ${selector}`);
return null;
}
// Function to wait for elements matching a condition
async function waitForElements(parent, checkFn, timeout = 5000) {
const start = Date.now();
while (Date.now() - start < timeout) {
const result = checkFn(parent);
if (result && result.length > 0) return result;
await new Promise(resolve => setTimeout(resolve, 100));
}
return [];
}
// Function to find element by aria label
function findElementByAriaLabel(container, labelText) {
if (!container || !labelText) {
debug('Invalid parameters for findElementByAriaLabel');
return null;
}
return Array.from(container.querySelectorAll('[aria-label]'))
.find(el => el.getAttribute('aria-label').toLowerCase().includes(labelText.toLowerCase()));
}
// Function to handle the compose window
async function handleComposeWindow(composeWindow) {
if (!composeWindow) {
console.error('Invalid compose window');
return;
}
// KNOWN LIMITATION: When multiple compose windows are open simultaneously,
// selecting a signature in a new window may overwrite signatures in previous windows.
// This is due to how Gmail's signature menu DOM elements are structured.
// For best results, compose emails one at a time.
// Check if we've already processed this window
const windowId = composeWindow.getAttribute('data-compose-id');
if (!windowId) {
debug('No compose ID found for window');
return;
}
if (processedWindows.has(windowId)) {
return;
}
processedWindows.add(windowId);
cleanupProcessedWindows();
debug('New compose window detected');
try {
// Wait for the compose window to be fully loaded
await new Promise(resolve => setTimeout(resolve, CONFIG.COMPOSE_WINDOW_LOAD_DELAY));
// Find the signature button using the exact selector
const signatureButton = await waitForElement(CONFIG.SELECTORS.SIGNATURE_BUTTON, composeWindow);
if (!signatureButton) {
console.warn('Signature button not found');
return;
}
debug('Found signature button');
simulateClick(signatureButton);
// Wait for signature menu to appear
await new Promise(resolve => setTimeout(resolve, CONFIG.SIGNATURE_MENU_DELAY));
// Validate menu presence
const menu = document.querySelector(CONFIG.SELECTORS.SIGNATURE_MENU);
if (!menu) {
console.error('Menu not found after clicking signature button');
return;
}
// Find the signature menu and options using exact selectors
const allOptions = Array.from(document.querySelectorAll(CONFIG.SELECTORS.SIGNATURE_OPTIONS));
// Create a Set to store unique signature texts to prevent duplicates
const uniqueSignatures = new Set();
// Filter signature options:
// 1. Exclude built-in options ("Manage signatures" and "No signature")
// 2. Prevent duplicates using the Set
// 3. You can add more signatures to ignore by adding them to CONFIG.IGNORED_SIGNATURES
//
// NOTE: For best results, when creating signatures in Gmail:
// - Use simple names without special characters
// - Avoid names that match the ignored signatures
// - Keep signature names unique and descriptive
const signatureOptions = allOptions.filter(option => {
const text = option.textContent.trim();
if (CONFIG.IGNORED_SIGNATURES.includes(text) || uniqueSignatures.has(text)) {
return false;
}
uniqueSignatures.add(text);
return true;
});
if (signatureOptions.length > 0) {
debug('Found signature options:', signatureOptions.map(opt => opt.textContent.trim()));
const randomIndex = Math.floor(Math.random() * signatureOptions.length);
const selectedSignature = signatureOptions[randomIndex];
debug('Selecting signature:', selectedSignature.textContent.trim());
simulateClick(selectedSignature);
} else {
console.warn('No valid signature options found');
}
} catch (error) {
console.error('Error in handleComposeWindow:', error);
}
}
// Function to observe for new compose windows
function observeForComposeWindows() {
debug('Starting compose window observer');
const observer = new MutationObserver((mutations) => {
mutations.forEach(mutation => {
mutation.addedNodes.forEach(node => {
if (node.nodeType === Node.ELEMENT_NODE) {
// Check for compose window
if (node.matches(CONFIG.SELECTORS.COMPOSE_WINDOW)) {
handleComposeWindow(node);
return;
}
// Also check children
const composeWindow = node.querySelector(CONFIG.SELECTORS.COMPOSE_WINDOW);
if (composeWindow) {
handleComposeWindow(composeWindow);
}
}
});
});
});
observer.observe(document.body, {
childList: true,
subtree: true
});
}
// Initialize when the page is ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', observeForComposeWindows);
} else {
observeForComposeWindows();
}