-
Notifications
You must be signed in to change notification settings - Fork 2
/
Copy pathgetDomainsDNS.gs
357 lines (309 loc) · 13.4 KB
/
getDomainsDNS.gs
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
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
/**
* This script retrieves domain information from Admin Directory API and performs
* DNS lookups using Google Public DNS. It was initially based on a script
* that used Cloudflare DNS
* (https://github.com/cloudflare/cloudflare-docs/blob/production/content/1.1.1.1/other-ways-to-use-1.1.1.1/dns-in-google-sheets.md),
* but has been significantly modified to remove Cloudflare DNS and integrate
* with Google Public DNS and Google Admin API.
* */
function getDomainList() {
const customerDomain = 'my_customer';
const spreadsheet = SpreadsheetApp.getActiveSpreadsheet();
let sheet = spreadsheet.getSheetByName('Domains/DNS');
try {
if (sheet) {
spreadsheet.deleteSheet(sheet);
}
sheet = spreadsheet.insertSheet('Domains/DNS', spreadsheet.getNumSheets());
const headers = ["Domains", "Verified", "Primary", "MX", "SPF", "DKIM", "DMARC", "DNS Status"];
sheet.getRange("A1:H1").setValues([headers])
.setFontColor("#ffffff")
.setFontSize(10)
.setFontFamily("Montserrat")
.setBackground("#fc3165")
.setFontWeight("bold");
sheet.getRange("D1").setNote("Mail Exchange, Red cells indicate Google MX records were not found.");
sheet.getRange("E1").setNote("Sender Policy Framework, Red cells indicate Google SPF record was not found.");
sheet.getRange("F1").setNote("DomainKeys Identified Mail, Red cells indicate the default DKIM selector for Google was not found.");
sheet.getRange("G1").setNote("Domain-based Message Authentication, Reporting, and Conformance, Red cells indicate no DMARC records found.");
sheet.getRange("H1").setNote("Status of DNS Lookups");
// Get the Domain
const domainList = getDomainInformation_(customerDomain);
// Write domain information to spreadsheet in one batch
sheet.getRange(2, 1, domainList.length, domainList[0].length).setValues(domainList);
// Get DNS Records
const dnsResultsWithStatus = getDnsRecords(domainList); // Modified to return DNS results AND status
// Get the last row
const lastRow = sheet.getLastRow();
//Extract DNS results
const dnsResults = dnsResultsWithStatus.map(item => [item.mxRecords.data, item.spfRecords.data, item.dkimRecords.data, item.dmarcRecords.data]);
sheet.getRange(2, 4, lastRow - 1, 4).setValues(dnsResults);
//Write the status message to the sheet
const statusMessages = dnsResultsWithStatus.map(item => {
let overallStatus = "";
if (item.mxRecords.status !== "Lookup Complete" ||
item.spfRecords.status !== "Lookup Complete" ||
item.dkimRecords.status !== "Lookup Complete" ||
item.dmarcRecords.status !== "Lookup Complete") {
//Something is not complete.
let issues = [];
if (item.mxRecords.status !== "Lookup Complete") {
issues.push(`MX: ${item.mxRecords.status}`);
}
if (item.spfRecords.status !== "Lookup Complete") {
issues.push(`SPF: ${item.spfRecords.status}`);
}
if (item.dkimRecords.status !== "Lookup Complete") {
issues.push(`DKIM: ${item.dkimRecords.status}`);
}
if (item.dmarcRecords.status !== "Lookup Complete") {
issues.push(`DMARC: ${item.dmarcRecords.status}`);
}
overallStatus = "Issues Found:\n" + issues.join("\n"); // Added line breaks
} else {
overallStatus = "Lookup Complete";
}
return [overallStatus];
});
sheet.getRange(2, 8, lastRow - 1, 1).setValues(statusMessages); // Write status messages to column H
// Delete columns I-Z
sheet.deleteColumns(9, 18);
// Delete empty rows
const dataRange = sheet.getDataRange();
const allData = dataRange.getValues();
for (let i = allData.length - 1; i >= 0; i--) {
if (allData[i].every(value => value === '')) {
sheet.deleteRow(i + 1);
}
}
// Set column sizes
sheet.autoResizeColumn(1);
sheet.setColumnWidth(4, 150);
sheet.setColumnWidth(5, 150);
sheet.setColumnWidth(6, 150);
sheet.setColumnWidth(7, 164); // Adjusted for Status Message
sheet.setColumnWidth(8, 300); // Adjusted for Status Message
// Apply conditional formatting rules
const rangeD = sheet.getRange("D2:D" + lastRow);
const ruleD = SpreadsheetApp.newConditionalFormatRule()
.whenTextContains("google")
.setBackground("#b7e1cd")
.setRanges([rangeD])
.build();
const rangeE = sheet.getRange("E2:E" + lastRow);
const ruleE = SpreadsheetApp.newConditionalFormatRule()
.whenTextContains("_spf.google.com")
.setBackground("#b7e1cd")
.setRanges([rangeE])
.build();
const rangeF = sheet.getRange("F2:F" + lastRow);
const ruleF = SpreadsheetApp.newConditionalFormatRule()
.whenTextContains("v=dkim1;")
.setBackground("#b7e1cd")
.setRanges([rangeF])
.build();
const rangeG = sheet.getRange("G2:G" + lastRow);
const ruleG = SpreadsheetApp.newConditionalFormatRule()
.whenTextContains("v=dmarc")
.setBackground("#b7e1cd")
.setRanges([rangeG])
.build();
const rangeDRed = sheet.getRange("D2:D" + lastRow);
const ruleDRed = SpreadsheetApp.newConditionalFormatRule()
.whenTextDoesNotContain("google")
.setBackground("#ffb6c1")
.setRanges([rangeD])
.build();
const rangeERed = sheet.getRange("E2:E" + lastRow);
const ruleERed = SpreadsheetApp.newConditionalFormatRule()
.whenTextDoesNotContain("_spf.google.com")
.setBackground("#ffb6c1")
.setRanges([rangeE])
.build();
const rangeFRed = sheet.getRange("F2:F" + lastRow);
const ruleFRed = SpreadsheetApp.newConditionalFormatRule()
.whenTextDoesNotContain("v=dkim1;")
.setBackground("#ffb6c1")
.setRanges([rangeF])
.build();
const rangeGRed = sheet.getRange("G2:G" + lastRow);
const ruleGRed = SpreadsheetApp.newConditionalFormatRule()
.whenTextDoesNotContain("v=dmarc")
.setBackground("#ffb6c1")
.setRanges([rangeG])
.build();
const rules = [ruleD, ruleE, ruleF, ruleG, ruleDRed, ruleERed, ruleFRed, ruleGRed];
sheet.setConditionalFormatRules(rules);
// --- Add Filter View ---
const filterRange = sheet.getRange('B1:H' + sheet.getLastRow());
filterRange.createFilter();
// --- Freeze Row 1 ---
sheet.setFrozenRows(1);
} catch (e) {
// Display error message to the user
SpreadsheetApp.getActiveSpreadsheet().toast(`An error occurred: ${e.message}`, 'Error', 5);
Logger.log(e);
}
}
function getDomainInformation_(customerDomain) {
const domainList = [];
let pageToken = null;
const maxRetries = 3;
// Retrieve domain information
do {
let retryCount = 0;
let success = false;
while (!success && retryCount < maxRetries) {
try {
const response = AdminDirectory.Domains.list(customerDomain, { pageToken: pageToken });
pageToken = response.nextPageToken;
if (!response || !response.domains) {
console.warn('No domains found or error retrieving domains.');
break;
}
response.domains.forEach(function (domain) {
domainList.push([domain.domainName, domain.verified, domain.isPrimary]);
});
success = true;
} catch (e) {
console.error(`Error retrieving domains (retry ${retryCount + 1}/${maxRetries}):`, e);
retryCount++;
Utilities.sleep(1000 * Math.pow(2, retryCount));
}
}
if (!success) {
console.error("Failed to retrieve domains after multiple retries.");
break;
}
} while (pageToken);
// Retrieve domain alias information
pageToken = null;
do {
let retryCount = 0;
let success = false;
while (!success && retryCount < maxRetries) {
try {
const response = AdminDirectory.DomainAliases.list(customerDomain, { pageToken: pageToken });
pageToken = response.nextPageToken;
if (response && response.domainAliases) {
response.domainAliases.forEach(function (domainAlias) {
domainList.push([domainAlias.domainAliasName, domainAlias.verified, 'False']);
});
}
success = true;
} catch (e) {
console.error(`Error retrieving domain aliases (retry ${retryCount + 1}/${maxRetries}):`, e);
retryCount++;
Utilities.sleep(1000 * Math.pow(2, retryCount));
}
}
if (!success) {
console.error("Failed to retrieve domain aliases after multiple retries.");
break;
}
} while (pageToken);
return domainList;
}
function getDnsRecords(domainList) {
const dnsResults = [];
const delayBetweenCalls = 100; // milliseconds
for (let i = 0; i < domainList.length; i++) {
const domain = domainList[i][0];
let mxRecords = performGoogleDNSLookup("MX", domain);
Utilities.sleep(delayBetweenCalls);
let spfRecords = performGoogleDNSLookup("TXT", domain);
Utilities.sleep(delayBetweenCalls);
let dkimRecords = performGoogleDNSLookup("TXT", "google._domainkey." + domain);
Utilities.sleep(delayBetweenCalls);
let dmarcRecords = performGoogleDNSLookup("TXT", "_dmarc." + domain);
Utilities.sleep(delayBetweenCalls);
mxRecords.data = mxRecords.data || "No MX records found";
spfRecords.data = spfRecords.data || "No SPF records found";
dkimRecords.data = dkimRecords.data || "No DKIM records found";
dmarcRecords.data = dmarcRecords.data || "No DMARC records found";
dnsResults.push({
mxRecords: mxRecords,
spfRecords: spfRecords,
dkimRecords: dkimRecords,
dmarcRecords: dmarcRecords
});
}
return dnsResults;
}
/**
* Performs a DNS lookup using Google Public DNS.
* @param {string} type The DNS record type (e.g., "MX", "TXT").
* @param {string} domain The domain to lookup.
* @return {object} An object with the DNS data and a status message.
**/
function performGoogleDNSLookup(type, domain) {
const url = `https://dns.google/resolve?name=${encodeURIComponent(domain)}&type=${encodeURIComponent(type)}`;
const maxRetries = 3;
let status = ""; // Intialize status
for (let retry = 0; retry <= maxRetries; retry++) {
try {
const options = {
muteHttpExceptions: true,
followRedirects: true // Important to handle redirects.
};
const response = UrlFetchApp.fetch(url, options);
const httpCode = response.getResponseCode();
const contentText = response.getContentText(); // Get content for error logging.
if (httpCode === 200) {
const data = JSON.parse(contentText);
if (data.Answer) {
const outputData = data.Answer.map(record => record.data);
return {
data: outputData.join('\n'),
status: "Lookup Complete" // Set successful status for this record type
};
} else {
// No Answer Section
return {
data: "",
status: "No records found" // Set status for no records found
}; // Return empty string for no record found
}
} else if (httpCode === 429) {
// Handle Too Many Requests
status = `Too Many Requests`; // Status message
Logger.warn(`Google Public DNS 429 Too Many Requests for ${domain} (${type}). Manual check recommended after a cool-down period.`);
const retryAfter = response.getHeaders()['Retry-After'];
let waitTime = retryAfter ? parseInt(retryAfter, 10) : 60; // Default to 60 seconds if header is missing
waitTime = Math.min(waitTime, 300); // Limit wait to 5 minutes.
Logger.log(`Waiting ${waitTime} seconds before retrying...`);
Utilities.sleep(waitTime * 1000); // Wait in milliseconds
// Do NOT return here. Let the retry happen.
} else if (httpCode === 500 || httpCode === 502) {
// Handle Internal Server Error and Bad Gateway (Retry)
status = `Internal Error`; // Status message
Logger.warn(`Google Public DNS HTTP Error ${httpCode} for ${domain} (${type}). Manual check recommended.`);
Utilities.sleep(1000 * Math.pow(2, retry)); // Exponential Backoff
// Do NOT return here. Let the retry happen.
} else if (httpCode === 400 || httpCode === 413 || httpCode === 414 || httpCode === 415) {
// Handle Permanent Errors (Don't Retry)
status = `Bad Request`; // Status message
Logger.error(`Google Public DNS HTTP Error ${httpCode} for ${domain} (${type}): ${contentText}`);
return { data: "", status: status }; // Don't retry bad request, payload too large etc.
} else if (httpCode === 301 || httpCode === 308) {
//Handle redirects and log.
status = `Redirect`; // Status message
Logger.log(`Google Public DNS HTTP Redirect ${httpCode} for ${domain} (${type}).`);
return { data: "", status: status }; //Return empty string since fetch is following redirects.
}
else {
// Unhandled HTTP Error
status = `Other Error`; // Status message
Logger.error(`Google Public DNS Unhandled HTTP Error ${httpCode} for ${domain} (${type}): ${contentText}`);
return { data: "", status: status };
}
} catch (error) {
status = `Exception`; // Status message
Logger.error(`Error fetching DNS data from Google Public DNS for ${domain} (${type}): ${error}`);
return { data: "", status: status };
}
} // End Retry Loop
status = `Multiple Retries Failed`; // Status Message
Logger.error(`Failed to retrieve DNS record for ${domain} (${type}) after multiple retries.`);
return { data: "", status: status };
}