Posts to Slack when new mail links to a site outside your expected list, with how recently that site was registered and who runs it.

UsesGmailIPWHOIS.ioRDAPSlack

Flag emails that link to unfamiliar websites

Whenever new mail arrives, the messages your Gmail search matches are read and one Slack note per message lists the hosts their links point to. Hosts on your list of expected domains, your own and your suppliers', are left out, so what reaches Slack is mail linking somewhere you did not expect.

For each host the note gives what the public registry holds: when the name was registered, how many days ago, and the registrar, or for a bare numeric address, who runs it and in which country. It also flags a link over plain http, a name in punycode, and a link on the sender's own domain. It reports and does not conclude: nothing says whether a link is worth opening, and no address is ever visited. Only the host part of a link is used, and a report that would repeat a full web address is withheld. Each message is labelled once read, so nothing is reported twice.

You need a Slack channel, a Gmail search naming the mail to read, and a list of the domains you already expect.

GmailGmailWhen an email arrives.When an email arrives.
ValuesSet mailSearch to blank.Set mailSearch to blank.
ValuesSet slackChannelId to blank.Set slackChannelId to blank.
ValuesSet expectedDomains to blank.Set expectedDomains to blank.
ValuesSet checkedLabel to Links checked.Set checkedLabel to Links checked.
ValuesSet maxMessagesPerRun to 10.Set maxMessagesPerRun to 10.
ValuesSet maxHostsPerMessage to 10.Set maxHostsPerMessage to 10.
TextJoin a list of [mailSearch, -label:, @checkedLabel, ] with nothing and save it as linkQuery.Join a list of [mailSearch, -label:, @checkedLabel, ] with nothing and save it as linkQuery.
MathSet messageLimit to maxMessagesPerRun multiplied by 1.Set messageLimit to maxMessagesPerRun multiplied by 1.
MathSet hostLimit to maxHostsPerMessage multiplied by 1.Set hostLimit to maxHostsPerMessage multiplied by 1.
TextSplit expectedDomains with "Split expectedDomains with replaced by , with replaced by in lowercase by , and save it as expectedList.
GmailGmailList emails matching linkQuery. Save it as arrived.List emails matching linkQuery. Save it as arrived.
ValuesSet readCount to 0.Set readCount to 0.
ValuesSet postCount to 0.Set postCount to 0.
Decisions & LoopsFor each message in arrived.emails:For each message in arrived.emails:
Decisions & LoopsIf readCount is greater than or equal to messageLimitIf readCount is greater than or equal to messageLimit
Yes
Decisions & LoopsStop repeating.Stop repeating.
No
No steps
Decisions & LoopsIf a step below fails, save the error as readErrorIf a step below fails, save the error as readError
Try
TextTrim message.id and save it as messageId.Trim message.id and save it as messageId.
Decisions & LoopsIf messageId is emptyIf messageId is empty
Yes
Decisions & LoopsSkip to the next item.Skip to the next item.
No
No steps
TextTrim message.from and save it as fromHeader.Trim message.from and save it as fromHeader.
ValuesSet addressPart to fromHeader.Set addressPart to fromHeader.
Decisions & LoopsIf fromHeader contains <If fromHeader contains <
Yes
ListsGet the first item of the last item of fromHeader split by < split by > and save it as addressPart.Get the first item of the last item of fromHeader split by < split by > and save it as addressPart.
No
No steps
TextMake the last item of addressPart split by @ lowercase and save it as senderDomain.Make the last item of addressPart split by @ lowercase and save it as senderDomain.
ValuesSet subject to (no subject).Set subject to (no subject).
Decisions & LoopsIf a step below fails, save the error as subjectMissingIf a step below fails, save the error as subjectMissing
Try
TextTrim message.subject and save it as subjectText.Trim message.subject and save it as subjectText.
Decisions & LoopsIf subjectText isn't empty and subjectText is not nullIf subjectText isn't empty and subjectText is not null
Yes
ValuesChange subject to subjectText.Change subject to subjectText.
No
No steps
On error
ValuesShow in the output: A message arrived with no subject..Show in the output: A message arrived with no subject..
ValuesSet bodyText to blank.Set bodyText to blank.
Decisions & LoopsIf a step below fails, save the error as bodyMissingIf a step below fails, save the error as bodyMissing
Try
TextTrim message.body and save it as bodyValue.Trim message.body and save it as bodyValue.
Decisions & LoopsIf bodyValue is not nullIf bodyValue is not null
Yes
ValuesChange bodyText to bodyValue.Change bodyText to bodyValue.
No
No steps
On error
ValuesShow in the output: A message arrived with no readable text..Show in the output: A message arrived with no readable text..
ListsStart an empty list and save it as seenHosts.Start an empty list and save it as seenHosts.
ListsStart an empty list and save it as hostLines.Start an empty list and save it as hostLines.
ValuesSet hostCount to 0.Set hostCount to 0.
Decisions & LoopsFor each linkUrl in all web addresses in bodyText:For each linkUrl in all web addresses in bodyText:
Decisions & Loops If hostCount is greater than or equal to hostLimit If hostCount is greater than or equal to hostLimit
Yes
Decisions & LoopsStop repeating.Stop repeating.
No
No steps
Text Trim linkUrl and save it as urlText. Trim linkUrl and save it as urlText.
Text Check whether urlText starts with http:// (ignoring case) and save it as plainHttp. Check whether urlText starts with http:// (ignoring case) and save it as plainHttp.
Text Join the items from index 1 onward of urlText split by :// with :// and save it as afterScheme. Join the items from index 1 onward of urlText split by :// with :// and save it as afterScheme.
Lists Get the first item of afterScheme split by / and save it as hostPart. Get the first item of afterScheme split by / and save it as hostPart.
Lists Get the first item of hostPart split by ? and save it as hostPart. Get the first item of hostPart split by ? and save it as hostPart.
Lists Get the first item of hostPart split by # and save it as hostPart. Get the first item of hostPart split by # and save it as hostPart.
Lists Get the last item of hostPart split by @ and save it as hostPart. Get the last item of hostPart split by @ and save it as hostPart.
Lists Get the first item of hostPart split by : and save it as hostPart. Get the first item of hostPart split by : and save it as hostPart.
Text Make hostPart lowercase and save it as linkHost. Make hostPart lowercase and save it as linkHost.
Decisions & Loops If linkHost is empty or linkHost doesn't contain . or linkHost contains : or… If linkHost is empty or linkHost doesn't contain . or linkHost contains : or…
Yes
Decisions & LoopsSkip to the next item.Skip to the next item.
No
No steps
Decisions & Loops If seenHosts contains linkHost If seenHosts contains linkHost
Yes
Decisions & LoopsSkip to the next item.Skip to the next item.
No
No steps
Lists Add linkHost to the end of seenHosts and save it as seenHosts. Add linkHost to the end of seenHosts and save it as seenHosts.
Values Set expected to false. Set expected to false.
Decisions & Loops For each allowed in expectedList: For each allowed in expectedList:
Text Trim allowed and save it as allowedName. Trim allowed and save it as allowedName.
Decisions & Loops If allowedName is empty or allowedName is null If allowedName is empty or allowedName is null
Yes
Decisions & LoopsSkip to the next item.Skip to the next item.
No
No steps
Decisions & Loops If linkHost is allowedName or linkHost ends with .@allowedName If linkHost is allowedName or linkHost ends with .@allowedName
Yes
ValuesChange expected to true.Change expected to true.
Decisions & LoopsStop repeating.Stop repeating.
No
No steps
Decisions & Loops If expected is yes If expected is yes
Yes
Decisions & LoopsSkip to the next item.Skip to the next item.
No
No steps
Values Add 1 to hostCount. Add 1 to hostCount.
Values Set notes to blank. Set notes to blank.
Decisions & Loops If plainHttp is yes If plainHttp is yes
Yes
ValuesChange notes to notes [linked over plain http].Change notes to notes [linked over plain http].
No
No steps
Decisions & Loops If linkHost starts with xn-- or linkHost contains .xn-- If linkHost starts with xn-- or linkHost contains .xn--
Yes
ValuesChange notes to notes [the name is written in punycode, xn--].Change notes to notes [the name is written in punycode, xn--].
No
No steps
Decisions & Loops If linkHost is senderDomain or linkHost ends with .@senderDomain If linkHost is senderDomain or linkHost ends with .@senderDomain
Yes
ValuesChange notes to notes [on the sender's own domain].Change notes to notes [on the sender's own domain].
No
No steps
Values Set factText to nothing was looked up for this host.. Set factText to nothing was looked up for this host..
Decisions & Loops If the first match in linkHost isn't empty If the first match in linkHost isn't empty
Yes
Decisions & LoopsIf a step below fails, save the error as ipErrorIf a step below fails, save the error as ipError
Try
IPWHOIS.ioIPWHOIS.ioGet the network operator of linkHost. Save it as net.Get the network operator of linkHost. Save it as net.
TextTrim net.isp and save it as isp.Trim net.isp and save it as isp.
TextTrim net.autonomous_system and save it as asn.Trim net.autonomous_system and save it as asn.
TextTrim net.country and save it as country.Trim net.country and save it as country.
TextJoin a list of [the link names an address rather than a name; the registry for that address records , isp, , , asn, , , country, .] with nothing and save it as factText.Join a list of [the link names an address rather than a name; the registry for that address records , isp, , , asn, , , country, .] with nothing and save it as factText.
On error
ValuesChange factText to the link names an address rather than a name, and the lookup for that address did not complete..Change factText to the link names an address rather than a name, and the lookup for that address did not complete..
No
Decisions & LoopsIf a step below fails, save the error as registryErrorIf a step below fails, save the error as registryError
Try
RDAPRDAPLook up domain linkHost. Save it as registry.Look up domain linkHost. Save it as registry.
ValuesSet record to registry.result.Set record to registry.result.
TextTrim record.registration_date and save it as registeredOn.Trim record.registration_date and save it as registeredOn.
TextTrim record.registrar and save it as registrar.Trim record.registrar and save it as registrar.
ValuesChange factText to the registry returned no registration date for this name..Change factText to the registry returned no registration date for this name..
Decisions & LoopsIf registeredOn isn't empty and registeredOn is not nullIf registeredOn isn't empty and registeredOn is not null
Yes
ValuesSet ageText to blank.Set ageText to blank.
Decisions & LoopsIf a step below fails, save the error as ageErrorIf a step below fails, save the error as ageError
Try
TextJoin a list of [, , the difference between registeredOn and today's date in days as text, days ago] with nothing and save it as ageText.Join a list of [, , the difference between registeredOn and today's date in days as text, days ago] with nothing and save it as ageText.
On error
ValuesChange ageText to blank.Change ageText to blank.
TextJoin a list of [the registry records this name as registered on , the first item of registeredOn split by T, ageText, .] with nothing and save it as factText.Join a list of [the registry records this name as registered on , the first item of registeredOn split by T, ageText, .] with nothing and save it as factText.
Decisions & LoopsIf registrar isn't empty and registrar is not nullIf registrar isn't empty and registrar is not null
Yes
TextJoin a list of [factText, Registrar of record: , registrar, .] with nothing and save it as factText.Join a list of [factText, Registrar of record: , registrar, .] with nothing and save it as factText.
No
No steps
No
No steps
On error
ValuesChange factText to the registry lookup for this name did not complete..Change factText to the registry lookup for this name did not complete..
Lists Add a list of [- , linkHost, notes, - , factText] joined by nothing to the end of hostLines and save it as hostLines. Add a list of [- , linkHost, notes, - , factText] joined by nothing to the end of hostLines and save it as hostLines.
Decisions & LoopsIf hostLines isn't emptyIf hostLines isn't empty
Yes
TextJoin a list of [a list of ["Links in mail that just arrivedJoin a list of [a list of [Links in mail that just arrived From: , fromHeader truncated to 120 characters using ... as the ellipsis, Subject: , subject truncated to 120 characters using ... as the ellipsis] joined by nothing, hostLines joined by a new line, Only the host part of each link is shown, and only hosts that are not on your 'expectedDomains' list. None of these lookups says what a link is - they report what the public registry holds about a name, and nothing more. A name with nothing on record is a name with nothing on record. Nothing here has visited any of these addresses.] with a blank line and save it as report.
Decisions & LoopsFor each reportLink in all web addresses in report:For each reportLink in all web addresses in report:
Text Replace reportLink with [link removed] in report and save it as report. Replace reportLink with [link removed] in report and save it as report.
Decisions & LoopsIf report contains ://If report contains ://
Yes
ValuesChange report to A message arrived carrying links to hosts outside your expected domains, but the report could not be written without repeating a web address from the message. Open the message in the mailbox instead..Change report to A message arrived carrying links to hosts outside your expected domains, but the report could not be written without repeating a web address from the message. Open the message in the mailbox instead..
No
No steps
SlackSlackSend a message to slackChannelId.Send a message to slackChannelId.report
ValuesAdd 1 to postCount.Add 1 to postCount.
No
ValuesShow in the output: a list of [Message , messageId, linked to nothing outside your expected domains, or its text held no web addresses.] joined by nothing.Show in the output: a list of [Message , messageId, linked to nothing outside your expected domains, or its text held no web addresses.] joined by nothing.
GmailGmailAdd label checkedLabel to email messageId.Add label checkedLabel to email messageId.
ValuesAdd 1 to readCount.Add 1 to readCount.
On error
ValuesShow in the output: Could not read the links out of one message: readError as text.Show in the output: Could not read the links out of one message: readError as text.
ValuesShow in the output: a list of [Messages read: , readCount as text, , reports posted: , postCount as text] joined by nothing.Show in the output: a list of [Messages read: , readCount as text, , reports posted: , postCount as text] joined by nothing.