For the emails your Gmail search picks out, logs whether each passed the sender checks and which servers it really came through.
Uses
Claude
Gmail
Google Sheetsip-api.com
Record where incoming emails really came from
Every six hours, the inbound mail matching a Gmail search you choose has its headers read, and one row per message is appended to your spreadsheet: what the receiving server logged for SPF, DKIM and DMARC, what the From and Return-Path headers say, which relay addresses appear, and where each of them resolves to.
It reports and does not conclude: no score, no rating, no verdict. Where a header records nothing, the row says so rather than reporting a failure, and a relay address in a data centre is flagged as exactly that, since much ordinary mail travels that way. Only headers are read; the body never reaches the spreadsheet or the AI. The last column is a short AI note describing the fields in that row; if it strays into judging the message it is set aside and the row says so. Each message is labelled 'Headers logged' once written, so nothing is logged twice.
Before the first run you need a spreadsheet tab headed for the columns each row fills.
mailSearch to blank.Set mailSearch to blank.spreadsheetUrl to blank.Set spreadsheetUrl to blank.tabName to blank.Set tabName to blank.checkedLabel to Headers logged.Set checkedLabel to Headers logged.maxMessages to 10.Set maxMessages to 10.maxRelayAddresses to 10.Set maxRelayAddresses to 10.checkedOn.Get today's date and save it as checkedOn.searchQuery to mailSearch -label:@checkedLabel.Set searchQuery to mailSearch -label:@checkedLabel.searchQuery. Save it as inbox.List emails matching searchQuery. Save it as inbox.messages to an empty list.Set messages to an empty list.inbox.emails isn't emptyIf inbox.emails isn't emptymessages to inbox.emails.Change messages to inbox.emails.maxMessages multiplied by 1 items of messages and save it as checkedMessages.Take the first maxMessages multiplied by 1 items of messages and save it as checkedMessages.checkedMessages is emptyIf checkedMessages is emptyNo mail matched that search that has not already been logged..Show in the output: No mail matched that search that has not already been logged..message in checkedMessages:For each message in checkedMessages:messageId to blank.Set messageId to blank.message.id isn't empty and message.id isn't blankIf message.id isn't empty and message.id isn't blankmessage.id and save it as messageId.Trim message.id and save it as messageId.messageId is emptyIf messageId is emptySkipped a message that arrived without an id..Show in the output: Skipped a message that arrived without an id..headerErrorIf a step below fails, save the error as headerErrormessageId. Save it as headers.Get the sender, subject and date of email messageId. Save it as headers.fromLine to no From header.Set fromLine to no From header.headers.From isn't empty and headers.From isn't blankIf headers.From isn't empty and headers.From isn't blankheaders.From and save it as fromLine.Trim headers.From and save it as fromLine.subjectLine to no Subject header.Set subjectLine to no Subject header.headers.Subject isn't empty and headers.Subject isn't blankIf headers.Subject isn't empty and headers.Subject isn't blankheaders.Subject and save it as subjectLine.Trim headers.Subject and save it as subjectLine.dateLine to no Date header.Set dateLine to no Date header.headers.Date isn't empty and headers.Date isn't blankIf headers.Date isn't empty and headers.Date isn't blankheaders.Date and save it as dateLine.Trim headers.Date and save it as dateLine.returnPathLine to no Return-Path header.Set returnPathLine to no Return-Path header.headers["Return-Path"] isn't empty and headers["Return-Path"] isn't blankIf headers["Return-Path"] isn't empty and headers["Return-Path"] isn't blankheaders["Return-Path"] and save it as returnPathLine.Trim headers["Return-Path"] and save it as returnPathLine.authLine to blank.Set authLine to blank.headers["Authentication-Results"] isn't empty and…If headers["Authentication-Results"] isn't empty and…headers["Authentication-Results"] and save it as authLine.Trim headers["Authentication-Results"] and save it as authLine.receivedLine to blank.Set receivedLine to blank.headers.Received isn't empty and headers.Received isn't blankIf headers.Received isn't empty and headers.Received isn't blankheaders.Received and save it as receivedLine.Trim headers.Received and save it as receivedLine.spfHeaderLine to blank.Set spfHeaderLine to blank.headers["Received-SPF"] isn't empty and headers["Received-SPF"] isn't blankIf headers["Received-SPF"] isn't empty and headers["Received-SPF"] isn't blankheaders["Received-SPF"] and save it as spfHeaderLine.Trim headers["Received-SPF"] and save it as spfHeaderLine.originIpLine to blank.Set originIpLine to blank.headers["X-Originating-IP"] isn't empty and headers["X-Originating-IP"] isn't…If headers["X-Originating-IP"] isn't empty and headers["X-Originating-IP"] isn't…headers["X-Originating-IP"] and save it as originIpLine.Trim headers["X-Originating-IP"] and save it as originIpLine.authText to authLine spfHeaderLine.Set authText to authLine spfHeaderLine.spfResult to no SPF result recorded in these headers.Set spfResult to no SPF result recorded in these headers.authText in lowercase and save it as spfResultMatch.Extract the first match from authText in lowercase and save it as spfResultMatch.spfResultMatch isn't emptyIf spfResultMatch isn't emptyspfResult to spfResultMatch.Change spfResult to spfResultMatch.dkimResult to no DKIM result recorded in these headers.Set dkimResult to no DKIM result recorded in these headers.authText in lowercase and save it as dkimResultMatch.Extract the first match from authText in lowercase and save it as dkimResultMatch.dkimResultMatch isn't emptyIf dkimResultMatch isn't emptydkimResult to dkimResultMatch.Change dkimResult to dkimResultMatch.dmarcResult to no DMARC result recorded in these headers.Set dmarcResult to no DMARC result recorded in these headers.authText in lowercase and save it as dmarcResultMatch.Extract the first match from authText in lowercase and save it as dmarcResultMatch.dmarcResultMatch isn't emptyIf dmarcResultMatch isn't emptydmarcResult to dmarcResultMatch.Change dmarcResult to dmarcResultMatch.scanText to receivedLineSet scanText to receivedLine
authText
originIpLine.scanText and save it as foundAddresses.Extract all matches from scanText and save it as foundAddresses.maxRelayAddresses multiplied by 1 items of foundAddresses with duplicates removed and save it as relayAddresses.Take the first maxRelayAddresses multiplied by 1 items of foundAddresses with duplicates removed and save it as relayAddresses.relayList to no relay address appears in these headers.Set relayList to no relay address appears in these headers.relayPlaces to no relay addresses to look up.Set relayPlaces to no relay addresses to look up.locatedRows to an empty list.Set locatedRows to an empty list.relayAddresses isn't emptyIf relayAddresses isn't emptyrelayAddresses with , and save it as relayList.Join relayAddresses with , and save it as relayList.lookupErrorIf a step below fails, save the error as lookupErrorlocated.Look up several IP addresses. Save it as located.located.results isn't emptyIf located.results isn't emptylocatedRows to located.results.Change locatedRows to located.results.relayPlaces to the address lookup step did not answer, so these addresses were not looked up.Change relayPlaces to the address lookup step did not answer, so these addresses were not looked up.placeLines to an empty list.Set placeLines to an empty list.relay in locatedRows:For each relay in locatedRows:relayQuery to an address. Set relayQuery to an address.relay.query isn't empty If relay.query isn't emptyrelay.query and save it as relayQuery.Trim relay.query and save it as relayQuery.relayPlace to not located. Set relayPlace to not located.relay.located is yes If relay.located is yesplaceLabel to no place on record.Set placeLabel to no place on record.relay.location_label isn't empty and relay.location_label isn't blankIf relay.location_label isn't empty and relay.location_label isn't blankrelay.location_label and save it as placeLabel.Trim relay.location_label and save it as placeLabel.ispName to no provider on record.Set ispName to no provider on record.relay.isp isn't empty and relay.isp isn't blankIf relay.isp isn't empty and relay.isp isn't blankrelay.isp and save it as ispName.Trim relay.isp and save it as ispName.relayPlace to placeLabel, run by ispName.Change relayPlace to placeLabel, run by ispName.notLocatedReason to no reason given.Set notLocatedReason to no reason given.relay.reason isn't empty and relay.reason isn't blankIf relay.reason isn't empty and relay.reason isn't blankrelay.reason and save it as notLocatedReason.Trim relay.reason and save it as notLocatedReason.relayPlace to not located (notLocatedReason).Change relayPlace to not located (notLocatedReason).relay.hosting is yes If relay.hosting is yesrelayPlace to relayPlace - a data centre address, which is what mail sent through a cloud or bulk provider looks like.Change relayPlace to relayPlace - a data centre address, which is what mail sent through a cloud or bulk provider looks like.relayQuery: relayPlace to the end of placeLines and save it as placeLines. Add relayQuery: relayPlace to the end of placeLines and save it as placeLines.placeLines isn't emptyIf placeLines isn't emptyplaceLines with a new line and save it as relayPlaces.Join placeLines with a new line and save it as relayPlaces.fromLine in lowercase and save it as fromDomain.Extract the first match from fromLine in lowercase and save it as fromDomain.returnPathLine in lowercase and save it as pathDomain.Extract the first match from returnPathLine in lowercase and save it as pathDomain.envelopeNote to A domain could not be read from both the From header and the Return-Path header..Set envelopeNote to A domain could not be read from both the From header and the Return-Path header..fromDomain isn't empty and pathDomain isn't emptyIf fromDomain isn't empty and pathDomain isn't emptyfromDomain is pathDomainIf fromDomain is pathDomainenvelopeNote to The From address and the Return-Path are on the same domain (fromDomain).Change envelopeNote to The From address and the Return-Path are on the same domain (fromDomain).envelopeNote to The From address is on fromDomain and the Return-Path is on pathDomain. These differ on most mailing lists, newsletters, ticketing systems and forwarded mail.Change envelopeNote to The From address is on fromDomain and the Return-Path is on pathDomain. These differ on most mailing lists, newsletters, ticketing systems and forwarded mail.factsSender to From: fromLineSet factsSender to From: fromLine
Return-Path: returnPathLine
envelopeNote.factsChecks to SPF: spfResultSet factsChecks to SPF: spfResult
DKIM: dkimResult
DMARC: dmarcResult.factsRelays to Relay addresses named in the headers: relayListSet factsRelays to Relay addresses named in the headers: relayList
Where those addresses resolve:
relayPlaces.headerFacts to factsSenderSet headerFacts to factsSender
factsChecks
factsRelays.notePrompt to You are writing one short note for a log of email header fields.Set notePrompt to You are writing one short note for a log of email header fields.
Describe only what the fields below record, in at most three sentences of plain English. Say when a field records nothing. Do not decide anything about the message, do not say what it is, and do not say what should be done about it. Do not mention anything that is not listed below.
Relay addresses in other countries are ordinary: mailing lists, forwarding services and cloud providers all route mail through them. Say where an address resolves without implying anything about it.
Header fields:
headerFacts.replyText to blank.Set replyText to blank.noteErrorIf a step below fails, save the error as noteErrornotePrompt. Save it as modelReply.Ask Claude: notePrompt. Save it as modelReply.
Maximum response length: 300modelReply.response_text isn't emptyIf modelReply.response_text isn't emptymodelReply.response_text and save it as replyText.Trim modelReply.response_text and save it as replyText.messageId.Show in the output: The written note step did not answer for message messageId.note to The written note step returned nothing, so this row carries the header fields on their own..Set note to The written note step returned nothing, so this row carries the header fields on their own..replyText isn't emptyIf replyText isn't emptynote to replyText.Change note to replyText.replyText in lowercase and save it as noteWentBeyondTheFields.Match keywords in replyText in lowercase and save it as noteWentBeyondTheFields.noteWentBeyondTheFields.is_match is yesIf noteWentBeyondTheFields.is_match is yesnote to The written note was set aside because it went beyond describing the fields. Read the header fields in this row instead..Change note to The written note was set aside because it went beyond describing the fields. Read the header fields in this row instead..tabName in spreadsheetUrl.Add a row to tabName in spreadsheetUrl.checkedLabel to email messageId.Add label checkedLabel to email messageId.messageId: headerError as text.Show in the output: Could not read the headers for message messageId: headerError as text.