Repository navigation
Expand file tree
/
Copy pathCode.gs
More file actions
138 lines (130 loc) · 5.65 KB
/
Copy pathCode.gs
File metadata and controls
138 lines (130 loc) · 5.65 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
function processDmarcReports() {
const LABEL_NAME = "DMARC-Report"; // Gmail label target
const RECIPIENT = Session.getActiveUser().getEmail();
let label = GmailApp.getUserLabelByName(LABEL_NAME);
if (!label) {
label = GmailApp.createLabel(LABEL_NAME);
}
const threads = label.getThreads(0, 25);
if (threads.length === 0) {
Logger.log("No unprocessed emails found under label: " + LABEL_NAME);
return;
}
let totalVolume = 0;
let totalPass = 0;
let totalFail = 0;
let failuresByIp = {};
let reportsProcessedCount = 0;
threads.forEach(thread => {
const messages = thread.getMessages();
messages.forEach(msg => {
const attachments = msg.getAttachments();
attachments.forEach(att => {
let xmlString = "";
const filename = att.getName().toLowerCase();
try {
if (filename.endsWith(".gz")) {
xmlString = Utilities.ungzip(att).getDataAsString();
} else if (filename.endsWith(".zip")) {
const unzipped = Utilities.unzip(att);
if (unzipped.length > 0) xmlString = unzipped[0].getDataAsString();
} else if (filename.endsWith(".xml")) {
xmlString = att.getDataAsString();
}
if (xmlString) {
reportsProcessedCount++;
parseDmarcXml(xmlString, function(count, spf, dkim, ip) {
totalVolume += count;
if (spf === "pass" && dkim === "pass") {
totalPass += count;
} else {
totalFail += count;
failuresByIp[ip] = (failuresByIp[ip] || 0) + count;
}
});
}
} catch (err) {
Logger.log("Error unpacking " + filename + ": " + err);
}
});
});
thread.removeLabel(label); // Ensures reports are only parsed once
});
if (reportsProcessedCount > 0) {
sendSummaryEmail(RECIPIENT, reportsProcessedCount, totalVolume, totalPass, totalFail, failuresByIp);
}
}
function parseDmarcXml(xmlString, recordCallback) {
const document = XmlService.parse(xmlString);
const root = document.getRootElement();
const records = root.getChildren("record");
records.forEach(record => {
const row = record.getChild("row");
if (!row) return;
const count = parseInt(row.getChildText("count") || "0", 10);
const ip = row.getChildText("source_ip") || "Unknown IP";
const policyEval = row.getChild("policy_evaluated");
let spf = "fail";
let dkim = "fail";
if (policyEval) {
spf = policyEval.getChildText("spf") || "fail";
dkim = policyEval.getChildText("dkim") || "fail";
}
recordCallback(count, spf, dkim, ip);
});
}
function getIpOrg(ip) {
try {
const response = UrlFetchApp.fetch("https://ipinfo.io/" + ip + "/json", { muteHttpExceptions: true });
if (response.getResponseCode() === 200) {
const data = JSON.parse(response.getContentText());
return data.org || data.hostname || "Unknown Provider";
}
} catch (e) {
Logger.log("Failed to resolve IP " + ip + ": " + e);
}
return "Unknown Provider";
}
function sendSummaryEmail(recipient, reportCount, volume, pass, fail, failuresByIp) {
const passPercent = volume > 0 ? Math.round((pass / volume) * 100) : 0;
let failureRows = "";
for (let ip in failuresByIp) {
const orgInfo = getIpOrg(ip);
failureRows += `
<tr>
<td style="padding: 8px; border-bottom: 1px solid #eee;">
<strong>${ip}</strong><br/>
<span style="font-size: 11px; color: #666;">${orgInfo}</span>
</td>
<td style="padding: 8px; border-bottom: 1px solid #eee; text-align: right; color: #d9534f; font-weight: bold;">
${failuresByIp[ip]}
</td>
</tr>`;
}
const htmlBody = `
<div style="font-family: Arial, sans-serif; max-width: 600px; color: #333;">
<h2 style="color: #2c3e50; border-bottom: 2px solid #3498db; padding-bottom: 8px;">DMARC Authentication Digest</h2>
<p>Processed <strong>${reportCount} report(s)</strong> from your inbox.</p>
<table style="width: 100%; border-collapse: collapse; margin-bottom: 20px;">
<tr style="background-color: #f8f9fa;">
<th style="padding: 8px; text-align: left;">Metric</th>
<th style="padding: 8px; text-align: right;">Count</th>
</tr>
<tr><td style="padding: 8px; border-bottom: 1px solid #eee;">Total Email Volume</td><td style="padding: 8px; border-bottom: 1px solid #eee; text-align: right;"><strong>${volume}</strong></td></tr>
<tr><td style="padding: 8px; border-bottom: 1px solid #eee; color: #2e7d32;">Authenticated (Pass)</td><td style="padding: 8px; border-bottom: 1px solid #eee; text-align: right; color: #2e7d32;"><strong>${pass} (${passPercent}%)</strong></td></tr>
<tr><td style="padding: 8px; border-bottom: 1px solid #eee; color: #c62828;">Unauthenticated (Fail)</td><td style="padding: 8px; border-bottom: 1px solid #eee; text-align: right; color: #c62828;"><strong>${fail}</strong></td></tr>
</table>
${fail > 0 ? `
<h3 style="color: #c62828; margin-top: 20px;">Unauthenticated Sources Breakdown</h3>
<table style="width: 100%; border-collapse: collapse;">
<tr style="background-color: #f8f9fa;">
<th style="padding: 8px; text-align: left;">Source IP & Provider</th>
<th style="padding: 8px; text-align: right;">Failed Messages</th>
</tr>
${failureRows}
</table>
` : '<p style="color: #2e7d32;"><strong>No authentication failures detected during this period.</strong></p>'}
</div>
`;
GmailApp.sendEmail(recipient, "DMARC Summary Report: " + passPercent + "% Pass Rate", "", { htmlBody: htmlBody });
}