email-and-phone.md (46741B)
1 --- 2 title: "Email & Phone Number OSINT" 3 description: "Validate an address or number, find the accounts attached to it, and pivot to a name — without alerting the owner." 4 category: osint 5 subcategory: "People & Identity" 6 tags: [osint, email, phone, pivoting] 7 tools: [epieos, ghunt, holehe, zehef, phoneinfoga, phunter, ignorant, libphonenumber, swaks, gravatar] 8 difficulty: intermediate 9 updated: 2026-10-04 10 references: 11 - name: "Bellingcat's Online Investigation Toolkit" 12 url: "https://bellingcat.gitbook.io/toolkit" 13 author: "Bellingcat" 14 license: none 15 relation: derived 16 note: "Tool catalogue: names, descriptions, cost flags and links for this area." 17 - name: "OSINT Newsletter Tools Library" 18 url: "https://tools.osintnewsletter.com" 19 author: "The OSINT Newsletter" 20 license: none 21 relation: derived 22 note: "Second tool catalogue, cross-checked against the above." 23 - name: "libphonenumber FAQ" 24 url: "https://github.com/google/libphonenumber/blob/master/FAQ.md" 25 author: "Google" 26 license: Apache-2.0 27 relation: link-only 28 note: "Upstream semantics for validity, carrier mapping and mobile number portability." 29 - name: "PhoneInfoga documentation" 30 url: "https://sundowndev.github.io/phoneinfoga/" 31 author: "Sundowndev" 32 relation: link-only 33 note: "Install methods, CLI flags and the scanner list were verified against this." 34 - name: "GHunt" 35 url: "https://github.com/mxrch/GHunt" 36 author: "mxrch" 37 relation: link-only 38 note: "Subcommand names and the three login methods were verified against this README." 39 - name: "Gravatar developer documentation: avatar images" 40 url: "https://docs.gravatar.com/api/avatars/images/" 41 author: "Automattic" 42 relation: link-only 43 note: "The d= and s= query parameters used for the existence probe." 44 - name: "Gravatar developer documentation: creating the hash" 45 url: "https://docs.gravatar.com/api/avatars/hash/" 46 author: "Automattic" 47 relation: link-only 48 note: "The trim, lower-case and SHA-256 recipe, and the worked example digest reproduced here." 49 - name: "Ofcom: telephone numbers for drama" 50 url: "https://www.ofcom.org.uk/phones-and-broadband/phone-numbers/numbers-for-drama/" 51 author: "Ofcom" 52 relation: link-only 53 note: "The UK number ranges reserved for fiction, used in the worked example." 54 --- 55 56 ## What this covers 57 58 An email address or phone number is usually the strongest pivot in an investigation, because 59 platforms treat them as account keys. From one address you can often reach a display name, a 60 profile photo, a set of registered services and sometimes a physical location. 61 62 What makes identifier work different from name work is that the answers are mostly binary and 63 mostly cheap. A name returns a ranked list of maybes; an identifier returns yes or no per service. 64 That is also the trap: a "no" from a service is four different claims wearing the same word, and 65 most of the mistakes on this page come from recording the wrong one. 66 67 ## Method 68 69 1. **Validate before enriching.** Confirm the address or number is structurally real and routable. 70 Time spent enriching a typo, a placeholder or a drama-range number is wasted, and the check 71 costs nothing. 72 2. **Normalise, then keep the original.** Decide which spellings are the same mailbox before you 73 count hits or deduplicate a list. Record the string as found as well as the normalised form — 74 the as-found spelling is what you search for in dumps and code. 75 3. **Take the free, non-interactive signals first.** DNS records, a Gravatar hash, provider-side 76 profile data. None of this touches the owner and none of it can be rate-limited into a false 77 negative. 78 4. **Then the registration oracles, knowing the cost.** Account-existence checks against signup 79 and password-reset flows reveal which services know the identifier, and some of them generate 80 mail or SMS to the target. 81 5. **Pivot outward as a plain string.** Code search, paste sites, breach indexes, the target's own 82 site, PDF metadata. Addresses get committed to repositories constantly, and numbers get left in 83 vCards and footers. 84 6. **Pivot inward to durable keys.** An address can change; a Google Gaia ID, a Telegram user ID 85 or an internal account ID cannot. Trade the identifier you were given for the one the platform 86 uses internally. 87 7. **Convert to a name, then switch approach.** Once you have a name, you are doing 88 [people search](/sheets/osint/people-search), not identifier work. 89 90 The judgement calls: decide before you start whether this is a passive engagement, because that one 91 decision rules out half the tools below and no tool will remind you. Decide whether you need the 92 subscriber or the account — a recovery phone, a work number and a partner's handset all answer 93 "what number is attached to this account" and none of them answers "what number does this person 94 carry". And decide what a negative is worth to you, because the cheapest way to be wrong here is to 95 write down "not registered" when what happened was a rate limit. 96 97 ## The existence oracle 98 99 Everything on the email side reduces to asking some service whether it knows a string, and the 100 useful part is knowing exactly which string you asked about. Providers normalise differently, so 101 the same mailbox has many spellings and every one of them is a distinct key to every tool, hash and 102 dataset you will use. 103 104 Gmail ignores dots in the local part — Google's own help states that if a sender "added dots to 105 your address, you'll still get that email" — and, like any provider implementing subaddressing, 106 treats everything from a `+` to the `@` as a tag on the same mailbox. So: 107 108 ```text 109 d.whitfield@example.com \ 110 dwhitfield@example.com > one mailbox, if the provider is Gmail 111 dwhitfield+shop@example.com / 112 113 ...and three unrelated keys, everywhere else: 114 115 sha256("d.whitfield@example.com") = fc9518a0411a19b071630d4b153e4774e0697053e4015e5eb9e1ab5c934021a3 116 sha256("dwhitfield@example.com") = 6a2ae976758ac8100467b0bea363301fcb6fe756c8262b95336be1d03dcbd919 117 sha256("dwhitfield+shop@example.com") = bbb5413ad03bc64773f79b7804cd4868f34e7fd7a5fa2e1808617ed45289369f 118 ``` 119 120 That matters immediately, because the cheapest non-interactive oracle in this whole area is a hash 121 lookup. Gravatar's documented recipe is: trim leading and trailing whitespace, force the string to 122 lower case, hash it with SHA-256, and request `https://gravatar.com/avatar/<hash>`. The `d=404` 123 parameter turns the avatar endpoint into an existence test — ask for a default image of `404` and 124 you get an HTTP status instead of a fallback picture. 125 126 ```bash 127 # the recipe, reproduced against the example in Gravatar's own documentation 128 printf '%s' "myemailaddress@example.com" | shasum -a 256 129 # 84059b07d4be67b806386c0aad8070a23f18836bbaae342275dc0a83414c32ee 130 131 # the probe: 200 means an avatar exists for that exact string, 404 means it does not 132 H=$(printf '%s' "d.whitfield@example.com" | shasum -a 256 | cut -d' ' -f1) 133 curl -s -o /dev/null -w '%{http_code}\n' "https://gravatar.com/avatar/$H?d=404&s=200" 134 # 404 135 ``` 136 137 Nothing reaches the owner, there is no rate limit worth worrying about, and a 200 hands you a 138 photograph plus, often, a WordPress-ecosystem account. Older code and older datasets used MD5 of 139 the same lower-cased string, which is why historic breach corpora and forum databases are full of 140 MD5 avatar URLs; the current documentation specifies SHA-256, so compute both when you are matching 141 against old data. A 404 speaks only for the exact string you hashed — run it again for the 142 dot-stripped and tag-stripped spellings before you call it absent. 143 144 The interactive oracles are the signup and password-reset flows, and their answers fall into four 145 classes that tools flatten into two: 146 147 ```text 148 what the flow says what you may record 149 -------------------------------------------- ------------------------------------------ 150 "we have sent a reset link to that address" the account exists, on this spelling 151 "no account found for that address" no account, on this spelling only 152 the same reassuring page for every input nothing -- the site is enumeration-hardened 153 429, captcha, timeout, silence nothing -- and you are now rate-limited 154 ``` 155 156 `holehe` and `ignorant` both print `[+]` for the first, `[-]` for the second and `[x]` for the 157 fourth, and the third is indistinguishable from the second unless you test with a control address 158 you know does not exist. Always run one. A `[-]` column with no control is an unmeasured 159 instrument. 160 161 ## E.164 arithmetic 162 163 The phone side has less to probe and more to compute. E.164 is the international format every tool 164 expects, and it decomposes into exactly three parts with a hard length cap: 165 166 ```text 167 +44 7700 900123 168 | | | 169 | | +-- subscriber number 170 | +-------- national destination code (here, a UK mobile block) 171 +------------ country code (1-3 digits) 172 173 E.164 allows at most 15 digits after the plus: 174 44 + 7700900123 = 2 + 10 = 12 digits, inside the limit 175 ``` 176 177 Two separate questions get confused here, and libphonenumber keeps them apart deliberately. 178 *Possible* means the digit count is plausible for that country. *Valid* means the number matches a 179 block the numbering plan actually defines. Neither means the number is assigned to a subscriber, 180 and nothing short of ringing it or paying for an HLR lookup establishes that it is in service. 181 182 ```text 183 +1 415 555 0123 possible: True valid: True <- and reserved for fiction 184 +44 7700 900123 possible: True valid: False <- Ofcom drama range 185 +44 7700 9001 possible: True valid: False <- IS_POSSIBLE_LOCAL_ONLY (4) 186 +44 770090012345 possible: False valid: False <- TOO_LONG (ValidationResult 3) 187 ``` 188 189 The first line is the one to remember. `+1 415 555 0123` passes validation because `555-01xx` sits 190 inside a legitimate NANP structure, while the North American numbering plan reserves `555-0100` 191 through `555-0199` for fictitious use. Ofcom is more honest about its equivalents and publishes 192 them: `07700 900000` to `900999` for mobile, `020 7946 0000` to `0999` for London, and similar 193 thousand-number blocks for other cities, with the note that "the numbers will not be allocated to 194 communications providers in the foreseeable future". Memorise both. A number from either space in 195 a dataset tells you the record is synthetic, and it tells you that for free in the first second of 196 the investigation. 197 198 The third line hides a second trap. `is_possible_number` returns `True` there not because the 199 length is right but because the library counts a number that could be dialled locally, without its 200 area code, as possible — the docstring says so outright. The reason code is 201 `IS_POSSIBLE_LOCAL_ONLY`, which is `4`; `TOO_SHORT` is `2` and `TOO_LONG` is `3`. The boolean 202 collapses all of that, so call `is_possible_number_with_reason` when you intend to write down why 203 a number failed rather than that it did. 204 205 The other arithmetic worth having is what a masked recovery number is actually worth. Reset screens 206 show you a tail — "we will text ●●●●●●23" — and the temptation is to enumerate forwards: 207 208 ```text 209 known prefix from a CV footer : 07700 9 (5 of 10 national digits) 210 masked tail from a reset page : 23 (2 of 10) 211 unknown digits : 10 - 5 - 2 = 3 212 candidates : 10^3 = 1000 <- far too many to probe quietly 213 ``` 214 215 So go the other way. Take a number you already have from an independent source and test whether it 216 *fits* the mask. That is a confirmation with a known false-positive rate: a two-digit tail matches 217 by chance one time in a hundred, which is weak on its own and strong in combination. A Google 218 two-digit tail and a courier SMS three-digit tail that both fit the same candidate is a one in 219 100,000 coincidence, and that is the kind of statement you can defend. 220 221 ## Email 222 223 ### Epieos 224 225 The first tool to reach for, because it is one query, it runs server-side and nothing it does 226 reaches the address. Enter the address at [epieos.com](https://epieos.com/) and it returns the 227 accounts it can see attached to the string — Google and Microsoft account data, a Gravatar hit, and 228 a spread of consumer services. The Google portion is the valuable half: a public account ID, the 229 profile photo, and whatever Maps or calendar activity the owner left visible. 230 231 ```text 232 1. Paste the address. Do the dot-stripped and tag-stripped spellings separately; the 233 back end is matching strings, not mailboxes. 234 2. Read the Google block first and copy the account ID out of it. That is the Gaia ID, 235 and it is the durable key -- feed it to `ghunt gaia` rather than re-querying the 236 address later. 237 3. Treat an empty module as "no answer", not "no account". The result page does not 238 distinguish a service that said no from a service that failed or was not checked on 239 your tier. 240 4. Archive the result page before you close it. These lookups are a snapshot of someone's 241 privacy settings on one day and are not reproducible later. 242 ``` 243 244 The free lookup returns a subset of the modules and the rest sits behind a paid tier, which is the 245 usual source of a phantom negative: a module you did not pay for looks exactly like a module that 246 found nothing. Capture evidence properly — see 247 [Archiving & Evidence](/sheets/osint/archiving-and-evidence) — because "Epieos said so last Tuesday" 248 is not a citation. 249 250 ### GHunt 251 252 Deeper on Google specifically, and the tool that converts an address into Google's internal account 253 ID. Install it with pipx and authenticate once; `ghunt login` offers three routes, and the 254 listening-mode handshake with the GHunt Companion browser extension is the one that does not 255 involve pasting cookies by hand. 256 257 ```bash 258 pipx install ghunt 259 ghunt login # listening mode, base64 cookies, or manual entry 260 261 # the subcommands, from --help, before you guess at one 262 ghunt -h # {login,email,gaia,drive,geolocate,spiderdal} 263 264 ghunt email target@gmail.com 265 ghunt email target@gmail.com --json target_email.json 266 267 # the internal account ID, which survives an address change 268 ghunt gaia 1234567890123456789 --json target_gaia.json 269 270 # a Drive file or folder ID: owner, permissions, and the other files it reveals 271 ghunt drive 1A2b3C4d5E6f7G8h9I0jKlMnOpQrStUv 272 273 # a BSSID, for when the pivot is a wireless network rather than a person 274 ghunt geolocate -b 00:11:22:33:44:55 275 ``` 276 277 Read the output in that order: the Gaia ID first, because it is the thing you cite and the thing 278 you pivot on; then the profile photo URL, which goes straight into 279 [reverse image search](/sheets/osint/reverse-image-search); then the service-by-service blocks. Use 280 `--json` on every run you intend to quote. The terminal rendering is for reading and the JSON is 281 for keeping, and the two are not equally easy to re-derive in six months. 282 283 How it misleads you: every query runs as an authenticated Google session, so use an account you are 284 willing to lose, and expect the unusual request volume to be exactly what Google's abuse systems 285 are built to notice. More subtly, an empty module reflects the target's visibility settings rather 286 than their absence — a person with a locked-down profile and a person with no Google account 287 produce output that looks similar at a glance. And the Drive module reports what your session can 288 see, so a file shared with your throwaway account and a genuinely public file are not distinguished 289 for you. 290 291 ### holehe 292 293 The broad sweep: it checks an address against 120-plus sites by watching how their registration and 294 forgotten-password flows respond. The positional argument takes more than one address, which is how 295 you run a whole normalisation set in a single pass. 296 297 ```bash 298 pipx install holehe # or: pip3 install holehe 299 300 holehe target@example.com 301 302 # several spellings at once -- the positional argument is variadic 303 holehe d.whitfield@example.com dwhitfield@example.com dwhitfield+shop@example.com 304 305 # only the hits, which is the only readable form on a 120-site run 306 holehe target@example.com --only-used 307 308 # skip the password-recovery methods: fewer sites checked, much less chance of mail 309 holehe target@example.com -NP 310 311 # keep the run: writes holehe_<timestamp>_<email>_results.csv in the working directory 312 holehe target@example.com -C 313 314 # slow or rate-limiting sites, and output that survives a pipe 315 holehe target@example.com -T 20 --no-color --no-clear 316 ``` 317 318 The output is one line per site, prefixed `[+]` used, `[-]` not used and `[x]` rate-limited. That 319 is the whole vocabulary — holehe's own legend prints those three and nothing else, so a module that 320 threw an exception is folded into one of them rather than flagged, and you cannot tell a broken 321 check from a clean negative by reading the terminal. Where a site leaks more than existence, 322 holehe appends it to the same line — a partially 323 masked recovery email, a masked phone number, sometimes a full name or an account creation date. 324 Those appended fragments are the most valuable thing the tool produces and the easiest to lose in 325 the scroll, which is the argument for `-C` and a look at the CSV rather than the terminal. 326 327 How it misleads you: upstream states the tool does not alert the target, and for most of the 120 328 sites that holds, but the underlying mechanism is the forgotten-password function — treat 329 "no notification" as a claim about the common case and not a guarantee, and reach for `-NP` when a 330 single unexpected reset email would end the engagement. Rate limiting is per egress IP and 331 accumulates across the run, so the sites checked last are the most likely to report `[x]`; a 332 second run from a different address will produce a different-looking result set from the same 333 truth. Never fold `[x]` into `[-]` when you write up. 334 335 ### Zehef 336 337 A second opinion with a different module mix, useful because its overlap with holehe is partial. 338 It runs paste-site and breach-index checks alongside account checks, and generates candidate 339 address combinations from a name. 340 341 ```bash 342 git clone https://github.com/N0rz3/Zehef.git 343 cd ./Zehef 344 pip3 install -r requirements.txt 345 346 python3 zehef.py target@example.com 347 python3 zehef.py -h 348 ``` 349 350 The combination generator is the part to be careful with. It emits plausible address spellings for 351 a person, which is a legitimate way to build a candidate list for the oracles above — and nothing 352 it emits is a confirmed address. Keep generated strings in a separate column from observed ones; a 353 generated address that later produces a `[+]` somewhere is a finding, and a generated address in a 354 report without that step is a fabrication. 355 356 ### Syntax, DNS and SMTP 357 358 Before any tool, ask whether the domain can receive mail at all. This is two DNS queries, it is 359 invisible to everybody, and it regularly ends the investigation early. 360 361 ```bash 362 # the mail exchangers for the domain 363 dig +noall +answer MX example.com 364 # example.com. 126 IN MX 0 . 365 366 # SPF, DMARC and the rest of the policy record set 367 dig +short TXT example.com 368 # "v=spf1 -all" 369 dig +short TXT _dmarc.example.com 370 ``` 371 372 A single MX of `0 .` is the null MX of RFC 7505: the domain is declaring that it accepts no mail 373 at all, so no mailbox on it exists, nothing was ever delivered to it and no account can have been 374 confirmed through it. Paired with `v=spf1 -all` — the domain sends no mail either — it means the 375 address in front of you is decorative. The opposite finding is just as useful: MX records pointing 376 at `*.l.google.com` or `*.outlook.com` tell you a custom-domain address is really a Google or 377 Microsoft account, which puts GHunt and the Microsoft modules back on the table for an address that 378 does not look like a free-mail one. 379 380 Confirming a specific mailbox, rather than the domain, means an SMTP conversation, and that is an 381 active step with a log entry at the other end. 382 383 ```bash 384 # RCPT probe: open the transaction, offer the recipient, quit before DATA. 385 # Nothing is sent. The server's reply to RCPT TO is the entire result. 386 swaks --to d.whitfield@example.com --from '<>' --quit-after RCPT --timeout 10 387 388 # client-side commands only, which is the readable form for a transcript 389 swaks --to d.whitfield@example.com --from '<>' --quit-after RCPT \ 390 --no-info-hints --hide-receive --hide-informational 391 392 # name the server explicitly when you want to probe one specific MX 393 swaks --to d.whitfield@example.com --from '<>' --quit-after RCPT --server mx1.example.net 394 ``` 395 396 The trap is the first line of output. Swaks resolves the recipient domain's MX itself using Perl's 397 `Net::DNS`, and when that module is missing it says so and falls back to localhost: 398 399 ```text 400 *** MX Routing not available: requires Net::DNS. Using localhost as mail server 401 === Trying localhost:25... 402 *** Error connecting to localhost:25: 403 *** Connection refused 404 ``` 405 406 That is not a result about the target. Install `Net::DNS` or pass `--server` from the `dig` output 407 above, and read the banner before you read the reply. Beyond that: a catch-all domain accepts every 408 recipient, so a `250` there means nothing; greylisting and tarpitting produce temporary rejections 409 that look like refusals; and large providers long ago stopped answering this question honestly. 410 Enumerating mailboxes through an authentication or delivery endpoint is also the step most likely 411 to be regulated where you are standing, so settle that before you type it, not after. DNS, WHOIS 412 and certificate-transparency work on the domain itself belongs on 413 [Websites & Infrastructure](/sheets/osint/websites-and-infrastructure). 414 415 ## Phone 416 417 ### phonenumbers (libphonenumber) 418 419 The Python port of Google's libphonenumber, and the correct first step for every number you touch. 420 It is offline, instant, free, and it answers the structural questions before you spend money or 421 attention on anything else. 422 423 ```bash 424 pip install phonenumbers 425 ``` 426 427 ```python 428 import phonenumbers 429 from phonenumbers import carrier, geocoder, timezone, PhoneNumberFormat, PhoneNumberType 430 431 # parse() needs a region hint for anything not in +E.164 form, and raises 432 # NumberParseException rather than guessing: 433 # phonenumbers.parse("020 7946 0123", None) 434 # -> NumberParseException: (0) Missing or invalid default region. 435 n = phonenumbers.parse("+447911123456", None) 436 437 print(phonenumbers.format_number(n, PhoneNumberFormat.E164)) # +447911123456 438 print(phonenumbers.format_number(n, PhoneNumberFormat.INTERNATIONAL)) # +44 7911 123456 439 print(phonenumbers.format_number(n, PhoneNumberFormat.NATIONAL)) # 07911 123456 440 441 print(phonenumbers.is_possible_number(n)) # True 442 print(phonenumbers.is_valid_number(n)) # True 443 print(phonenumbers.number_type(n) == PhoneNumberType.MOBILE) # True 444 445 print(carrier.name_for_number(n, "en")) # JT 446 print(geocoder.description_for_number(n, "en")) # Guernsey 447 print(timezone.time_zones_for_number(n)) 448 # ('Europe/Guernsey', 'Europe/Isle_of_Man', 'Europe/Jersey', 'Europe/London') 449 450 print(phonenumbers.region_code_for_number(n)) # GG 451 print(phonenumbers.is_valid_number_for_region(n, "GB")) # False 452 print(phonenumbers.is_mobile_number_portable_region("GB")) # True 453 454 # why a number failed, rather than just that it did 455 z = phonenumbers.parse("+44770090012345", None) 456 print(phonenumbers.is_possible_number_with_reason(z)) # 3 (TOO_LONG) 457 458 # a known-good number for a region, to sanity-check your own parsing 459 print(phonenumbers.format_number(phonenumbers.example_number("GB"), 460 PhoneNumberFormat.E164)) # +441212345678 461 ``` 462 463 That output contains the lesson. A number that looks like an ordinary UK mobile resolves to region 464 `GG`, carrier `JT` and four candidate timezones, because `+44` is shared between the United 465 Kingdom, Guernsey, Jersey and the Isle of Man. If you had written "UK subscriber" from the `+44` 466 you would have the wrong jurisdiction, the wrong regulator and the wrong carrier to serve anything 467 on. `number_type` returns `PhoneNumberType.UNKNOWN`, which is `99` and not `0` — `0` is 468 `FIXED_LINE`, so a truthiness check on the result is silently wrong. 469 470 How it misleads you: the carrier field is the *original* carrier. Upstream is explicit that for 471 regions supporting portability "we return the original carrier for the supplied number", so in any 472 country with mobile number portability — which `is_mobile_number_portable_region` will tell you 473 about — the carrier name is a historical fact about a block, not a current fact about a subscriber. 474 An empty string means the mapping has no data, not that the number has no carrier. The geocoder 475 returns a numbering-plan area, which for a fixed line is roughly where the line is and for a mobile 476 is roughly where the block was issued; neither is a residence. And none of it speaks to whether 477 the number is in service. 478 479 ### PhoneInfoga 480 481 Format normalisation, carrier and line-type identification, and footprint searching, wrapped in a 482 CLI and a local REST API. It is a Go binary: the documented installs are the release archive, 483 Homebrew and Docker, and there is no pip package to reach for. 484 485 ```bash 486 brew install phoneinfoga 487 # or: bash <( curl -sSL https://raw.githubusercontent.com/sundowndev/phoneinfoga/master/support/scripts/install ) 488 # sudo install ./phoneinfoga /usr/local/bin/phoneinfoga 489 # or: docker pull sundowndev/phoneinfoga:latest 490 491 phoneinfoga version 492 phoneinfoga scan -n "+14155550123" 493 phoneinfoga scan -n "+1 (555) 444-1212" # ( ) - + are escaped for you 494 495 # the local web UI and REST API on port 5000, or a port you choose 496 phoneinfoga serve 497 phoneinfoga serve -p 8080 498 phoneinfoga serve --no-client # REST API only, no web client 499 500 # a compiled custom scanner 501 phoneinfoga scan -n "+14155550123" --plugin ./custom_scanner.so 502 503 # docker equivalents 504 docker run --rm -it sundowndev/phoneinfoga version 505 docker run --rm -it -p 5000:5000 sundowndev/phoneinfoga serve 506 ``` 507 508 Five scanners ship with it — Local, Numverify, Googlesearch, Googlecse and OVH — and they are 509 configured by environment variable, not by flag: 510 511 ```bash 512 export NUMVERIFY_API_KEY=... # Numverify scanner 513 export GOOGLE_API_KEY=... # Googlecse scanner 514 export GOOGLECSE_CX=... # Googlecse search engine ID 515 export GOOGLECSE_MAX_RESULTS=50 # optional, default 10, maximum 100 516 ``` 517 518 Local and Googlesearch and OVH need no configuration, which means an unconfigured install silently 519 runs a subset: a scan with no `NUMVERIFY_API_KEY` set is not a scan that found no carrier. Read the 520 country code as mandatory — the docs say it plainly — because a national-format number scanned 521 without one produces confident output about the wrong country. The Googlesearch scanner's value is 522 a list of dork URLs you still have to open and judge yourself; treat it as a worklist, not a 523 finding. And the local scanner is numbering-plan metadata, so it inherits every portability caveat 524 from the section above. 525 526 ### Phunter 527 528 A number-oriented sweep with a reverse-lookup module and an Amazon account check, which is a 529 different and narrower bet than PhoneInfoga's. 530 531 ```bash 532 git clone https://github.com/N0rz3/Phunter.git 533 cd ./Phunter 534 pip3 install -r requirements.txt 535 536 python3 phunter.py -t "+33644637111" # operator, location, line type, reputation 537 python3 phunter.py -f numbers.txt # a file of numbers 538 python3 phunter.py -a "+33644637111" # is the number attached to an Amazon account 539 python3 phunter.py -p "+33644637111" # reverse lookup: who the number belongs to 540 python3 phunter.py -p "+33644637111" -o owner.txt 541 python3 phunter.py -v # version and service status 542 ``` 543 544 The `-v` check is worth running first on a tool of this kind: these modules break when a target 545 site changes its flow, and a module that is quietly broken returns the same empty result as a 546 module that found nothing. `-a` and `-p` are the two that matter and they are the two that reach 547 out — the Amazon check exercises an account flow, and the reverse lookup queries third-party 548 directories whose coverage collapses outside a handful of countries. A "reputation" or spam flag 549 here describes crowd-sourced complaints about the number, which is evidence about the number's 550 behaviour and not about its owner. 551 552 ### ignorant 553 554 The phone-side counterpart to holehe, by the same author and with the same flag vocabulary. It 555 checks whether a number is registered on a short list of sites — the README names Snapchat and 556 Instagram — and the positional arguments are the part people get wrong. 557 558 ```bash 559 pip3 install ignorant 560 561 # country code WITHOUT the plus, then the national number WITHOUT the trunk zero 562 ignorant 33 644637111 # = +33 6 44 63 71 11 563 ignorant 44 7911123456 # = +44 7911 123456, not "07911 123456" 564 565 ignorant 33 644637111 --only-used 566 ignorant 33 644637111 -T 20 --no-color --no-clear 567 ``` 568 569 Same prefixes as holehe: `[+]` registered, `[-]` not registered, `[x]` rate-limited. Same 570 discipline applies — it works by poking registration flows, so it is active, and a short result 571 set from a long run usually means you were throttled rather than that the number is unused. Because 572 the site list is small, a clean sweep of negatives is close to worthless on its own; its use is 573 confirmation when you already have a candidate and want a second thread tying it to an account. 574 575 ### Telegram Phone Number Checker 576 577 Bellingcat's tool for the single most productive messenger check, because Telegram will confirm 578 registration and often hand over a username, a display name and a user ID. 579 580 ```bash 581 pip install telegram-phone-number-checker 582 # with proxy support: pip install telegram-phone-number-checker[proxy] 583 584 # credentials come from https://my.telegram.org/ and live in a .env file, 585 # or on the command line 586 telegram-phone-number-checker --phone-numbers "+447911123456,+33644637111" 587 588 telegram-phone-number-checker --phone-numbers "+447911123456" \ 589 --download-profile-photos --output whitfield.json 590 591 telegram-phone-number-checker --usernames "someuser,otheruser" 592 593 telegram-phone-number-checker --phone-numbers "+447911123456" \ 594 --api-id 123456 --api-hash 0123456789abcdef0123456789abcdef \ 595 --api-phone-number "+447700900123" 596 597 telegram-phone-number-checker --phone-numbers "+447911123456" --proxy socks5://127.0.0.1:9050 598 ``` 599 600 The user ID in the output is the durable key — usernames and display names change weekly, the ID 601 does not — and the profile photo is a direct pivot to 602 [reverse image search](/sheets/osint/reverse-image-search) and to 603 [Social Media Platforms](/sheets/osint/social-media-platforms) for the rest of the account. 604 605 How it misleads you: the lookup runs as your own Telegram account, and upstream advises plainly 606 "not to use your personal account for automations as telegram may block it". A result of 607 "no username detected" is a privacy setting, not an absence of an account, and an error line means 608 the account exists but has restricted phone-number discovery — which is itself a finding about how 609 careful the owner is. Default `--output` is `results.json` in the working directory, so name it per 610 target or you will overwrite the last run. 611 612 ### Caller-ID and directory databases 613 614 Crowd-sourced caller-ID apps hold name data for numbers that appear nowhere else, because their 615 users uploaded their own address books. 616 617 | Tool | What it does | Cost | 618 | --- | --- | --- | 619 | [TrueCaller](https://www.truecaller.com/) | Large crowd-sourced caller-ID database. Names come from other users' contact lists, so accuracy varies and the entry may be a nickname. | partly free | 620 | [GetContact](https://www.getcontact.com/) | Same model, stronger coverage across Turkey, the Middle East and Central Asia. | partly free | 621 | [ThisNumber](https://www.thisnumber.com/) | International phone directory listings. | free | 622 | [NigeriaPhonebook](https://nigeriaphonebook.com/) | Nigerian number-to-name lookups. | free | 623 624 Searching a number in these apps can be visible to other users of the same app, and uploading a 625 contact list to get access hands over everyone in your phone. Use a clean device. Read a hit as 626 "at least one stranger saved this number under this label at some point", which is a real signal 627 and is not identification — the same number commonly returns a personal name, a trade name and an 628 insult, all from different users, all years apart. 629 630 ### Messaging-app checks 631 632 Many messengers confirm whether a number is registered, and often expose a profile photo and 633 status. Adding the number as a contact may make you visible to them — check the platform's 634 behaviour before you do it on a target who might notice. Do these from a dedicated device and 635 number: the contact list on the account you use for checks is itself a disclosure, and some 636 platforms surface "contacts who joined" to the people you looked up. 637 638 ## Identifier tools at a glance 639 640 | Tool | Identifier | What it does | Touches the target | 641 | --- | --- | --- | --- | 642 | [Epieos](https://epieos.com/) | email, phone | One server-side query across Google, Microsoft, Gravatar and consumer services. | no | 643 | [GHunt](https://github.com/mxrch/GHunt) | email, Gaia ID | Google account depth: internal ID, photo, Maps, calendar, Drive. | no, but runs as your Google session | 644 | Gravatar hash probe | email | Deterministic SHA-256 lookup for an avatar and a linked profile. | no | 645 | `dig` MX/TXT | email domain | Whether the domain can receive mail at all, and who runs it. | no | 646 | [holehe](https://github.com/megadose/holehe) | email | Registration and reset-flow checks across 120+ sites. | yes — may generate mail | 647 | [Zehef](https://github.com/N0rz3/Zehef) | email | Paste and breach checks, account checks, address generation. | partly | 648 | [swaks](https://www.jetmore.org/john/code/swaks/) | email | SMTP RCPT probe against the real mail exchanger. | yes — logged at the server | 649 | [phonenumbers](https://github.com/daviddrysdale/python-phonenumbers) | phone | Offline parse, validity, type, carrier block, timezone. | no | 650 | [PhoneInfoga](https://github.com/sundowndev/phoneinfoga) | phone | Normalisation plus scanner modules and a local REST API. | no for Local, yes for footprint scanners | 651 | [Phunter](https://github.com/N0rz3/Phunter) | phone | Line metadata, reverse lookup, Amazon account check. | yes for `-a` and `-p` | 652 | [ignorant](https://github.com/megadose/ignorant) | phone | Registration checks on a small set of sites. | yes | 653 | [Telegram Phone Number Checker](https://github.com/bellingcat/telegram-phone-number-checker) | phone | Telegram registration, username, display name, user ID. | yes — runs as your account | 654 655 ## Pitfalls 656 657 - **"Valid" does not mean "assigned".** `+1 415 555 0123` validates cleanly and is reserved for 658 fiction. Validity is a statement about the numbering plan, nothing more; service status costs 659 money or a ring. 660 - **Possible is weaker than valid, and both are weaker than real.** A number can be possible, 661 invalid and still in a dataset as a deliberate placeholder. 662 - **`+44` is not the United Kingdom.** It is shared with Guernsey, Jersey and the Isle of Man, and 663 `region_code_for_number` will tell you which — after which your jurisdiction, regulator and 664 carrier are all different. 665 - **Carrier data is pre-portability.** In a portable region the lookup returns the carrier the 666 block was issued to, which may be two providers out of date. An empty carrier field means no 667 data, not no carrier. 668 - **Reset-flow probing is active.** holehe, ignorant, Phunter's `-a` and any SMTP probe can 669 generate mail or a log entry. Passive-only work means Epieos, GHunt, DNS, hash probes and string 670 searching, nothing that touches a login flow. 671 - **`[x]` is not `[-]`.** Rate-limited is not absent. Rate limits are per egress IP and get worse 672 through a run, so the sites checked last are the least trustworthy in the output, and a second 673 run from elsewhere will disagree with the first. 674 - **Run a control address.** Enumeration-hardened sites return the same friendly page for every 675 input. Without a known-nonexistent control in the same run, you cannot tell that apart from a 676 negative. 677 - **Null MX is a finding, not an error.** An MX of `0 .` means the domain accepts no mail, so the 678 address never worked. Catch-all domains are the mirror image: they accept everything, so 679 "the address exists" is meaningless on them. 680 - **One mailbox, many keys.** Dots and `+tags` are the same Gmail inbox and completely different 681 hashes, dataset rows and tool inputs. Normalise before you count, and probe every spelling 682 before you record an absence. 683 - **A masked tail is a 1-in-100 coincidence.** Two digits of a recovery number matching your 684 candidate is weak evidence. Two independent masks from different services multiplying out to 685 1 in 100,000 is strong. Say which one you have. 686 - **A recovery phone is not the subject's phone.** It is frequently a partner's, a parent's, a 687 former employer's desk line or a long-dead PAYG handset kept for exactly this purpose. 688 - **Caller-ID names are user-submitted.** "John Smith Plumber" may be what one stranger saved the 689 number as years ago, and the lookup itself may be visible to that app's other users. 690 - **Recycled numbers and recycled mailboxes.** Carriers reissue numbers after a few months of 691 disuse and some providers release abandoned usernames, so an old registration may belong to 692 someone unrelated — and account-existence output will not distinguish them. 693 - **Aggregated identifiers age badly.** An address that reached someone in 2019 may be dead now; 694 date every lookup and quote the date beside the claim. 695 - **Enumeration may be illegal where you are.** Probing authentication endpoints for account 696 existence is regulated in some jurisdictions regardless of intent. Settle that before the first 697 command, not in the write-up. 698 699 ## Worked example 700 701 One datum: a tip-off naming **Dana Whitfield**, with one email address and one phone number and no 702 other detail — `d.whitfield@example.com` and `+44 7700 900123`. The task is to decide whether this 703 record is worth an investigation before anyone spends a day on it. 704 705 1. **Parse the number offline, first.** No network, no cost, no trace. 706 707 ```text 708 input : +447700900123 709 E164 : +447700900123 710 INTERNATIONAL : +44 7700 900123 711 NATIONAL : 07700 900123 712 country_code : 44 national_number: 7700900123 713 is_possible : True 714 is_valid : False 715 number_type : 99 (PhoneNumberType.UNKNOWN; MOBILE would be 1) 716 carrier : '' 717 geocoder : '' 718 timezones : ('Etc/Unknown',) 719 region : None 720 ``` 721 722 2. **Read the disagreement.** Possible but not valid, with no region, no carrier and 723 `Etc/Unknown` for a timezone. Twelve digits in the right shape for `+44`, matching no block the 724 numbering plan defines. That pattern is a reserved or unallocated range, not a typo. 725 3. **Name the range.** The national number is `7700900123`, which sits inside `07700 900000` to 726 `07700 900999` — Ofcom's drama block, 1,000 numbers that Ofcom states "will not be allocated to 727 communications providers in the foreseeable future". The number is fictional by construction. 728 4. **Do not take a tool's word for the conclusion.** Running a registration oracle against it 729 returns negatives, and those negatives are worthless: a rate limit, a privacy setting and an 730 unassigned number all print the same thing. The load-bearing evidence is the published range, 731 which anyone can check in a browser, not a `[-]` in a terminal. 732 5. **Switch to the address and ask the cheap question.** Two DNS queries, invisible to everyone: 733 734 ```text 735 $ dig +noall +answer MX example.com 736 example.com. 126 IN MX 0 . 737 738 $ dig +short TXT example.com | grep spf 739 "v=spf1 -all" 740 ``` 741 742 The `grep` is not decoration. A bare `TXT` query on a live domain also returns whatever 743 domain-verification tokens are parked there that week, and those rotate, so filtering is what makes 744 the transcript something a reader can reproduce rather than something they have to trust. The MX 745 TTL will differ from the one above for the same reason. 746 747 6. **Read it.** A single MX of `0 .` is RFC 7505's null MX — the domain publishes that it accepts 748 no mail — and `v=spf1 -all` says it sends none either. No mailbox on that domain has ever 749 received anything, so no service can have confirmed an account through it. 750 7. **Confirm with the deterministic probe, on every spelling.** SHA-256 of the trimmed, lower-cased 751 address, against the avatar endpoint with `d=404`: 752 753 ```text 754 d.whitfield@example.com fc9518a0411a19b071630d4b153e4774e0697053e4015e5eb9e1ab5c934021a3 HTTP 404 755 dwhitfield@example.com 6a2ae976758ac8100467b0bea363301fcb6fe756c8262b95336be1d03dcbd919 HTTP 404 756 dwhitfield+shop@example.com bbb5413ad03bc64773f79b7804cd4868f34e7fd7a5fa2e1808617ed45289369f HTTP 404 757 ``` 758 759 8. **Total cost.** Four lookups, no packet sent to any mail server, nothing delivered to anybody, 760 about ninety seconds. Both identifiers in the tip-off come from reserved or 761 non-deliverable space, and they come from two different reserved spaces, which is the signature 762 of a synthetic record rather than a transcription error. 763 9. **What the same pipeline looks like on a live record,** for contrast: `+44 7911 123456` parses 764 as valid, `PhoneNumberType.MOBILE`, carrier `JT`, geocoder `Guernsey`, region `GG`. That is the 765 point at which the oracles are worth running — and the point at which you must notice that your 766 `+44` number is not a GB number at all. 767 768 What you can assert: that the phone number lies inside a block Ofcom publishes as reserved for 769 drama and unallocated, and that the email domain publishes a null MX and an SPF record refusing all 770 sending. Both claims are independently checkable in under a minute by anyone reading the report, 771 and together they say the record is synthetic. The names, dates and timestamps of the two DNS 772 queries go in the notes, because DNS is mutable and a null MX today is not a null MX last year. 773 774 What would falsify it: Ofcom reallocating the drama range, which it says it will not do but which 775 is a policy rather than a law of nature; a null MX that was added after the record was created, 776 which would mean the address had worked earlier and is the single most likely way this conclusion 777 is wrong; or the tip-off being a mangled transcription of a real number one digit away from this 778 one. The weakest measurement is the Gravatar probe — a 404 speaks only for the exact strings 779 hashed, and `d.whitfield` and `dwhitfield` produce entirely unrelated digests, so three 404s cover 780 three spellings and not a mailbox. Quote it as "no avatar for these three spellings", never as 781 "no account". And none of this says anything at all about whether a person called Dana Whitfield 782 exists; it says the two identifiers attached to the name do not. Converting a name into a person is 783 [people search](/sheets/osint/people-search), and converting a handle into one is 784 [Usernames & Accounts](/sheets/osint/usernames-and-accounts). 785 786 ## Broader catalogues 787 788 - [Email Address OSINT](https://tools.osintnewsletter.com/tool-categories/email-address-osint) 789 - [Phone Number OSINT](https://tools.osintnewsletter.com/tool-categories/phone-number-osint) 790 791 792 ## More tools 793 794 Further tools for this area from the OSINT Newsletter Tools Library ([Email Address OSINT](https://tools.osintnewsletter.com/tool-categories/email-address-osint), [Phone Number OSINT](https://tools.osintnewsletter.com/tool-categories/phone-number-osint)), excluding those already listed above. 795 796 | Tool | What it does | 797 | --- | --- | 798 | [Aeroleads](https://aeroleads.com/) | A prospecting and contact discovery platform that helps users find professional email addresses, phone numbers, company… | 799 | [Analyst Research Tools](https://analystresearchtools.com/) | A browser-based investigative research platform pulling together a collection of free lookup tools to help you gather, organise… | 800 | [BeenVerified](https://www.beenverified.com/) | People-search and background research platform that aggregates publicly available records into a single interface. | 801 | [Behind the Email](https://behindtheemail.com) | A browser-based OSINT tool that identifies the online footprint of an email address by checking where it has been used across… | 802 | [LoLArchiver](https://lolarchiver.com/) | A niche OSINT tool designed to archive and surface user-related data across gaming and online platforms like League of Legends… | 803 | [OSINT Industries](https://app.osint.industries/) | An all-encompassing OSINT platform that gathers and correlates publicly available digital data such as emails, domains, phone… | 804 | [Osintly](https://osint.ly/) | A unified OSINT platform that searches for information across pseudonyms, email addresses, phone numbers, IP addresses, domains… | 805 | [Phone Validator](https://www.phonevalidator.com/index.aspx) | A phone-number validation & lookup service that can identify a number's line type, carrier/service provider & approx. location… | 806 | [Phunter](https://github.com/N0rz3/Phunter) | Allows you to find various information via a phone number. | 807 | [Sherlock Eye](https://www.sherlockeye.io/) | AI-powered OSINT assistant that automates investigations, links data, and surfaces insights fast. | 808 | [SynapsInt](https://synapsint.com/) | A web-based search platform that aggregates publicly available data about people, organisations, domains, IP addresses, email… | 809 | [Telegram Phone Number Checker](https://github.com/bellingcat/telegram-phone-number-checker?tab=readme-ov-file) | Identify a Telegram user by their phone number. | 810 | [Zehef](https://github.com/N0rz3/Zehef) | An email-focused OSINT tool that automates account discovery, breach checks, and paste site lookups | 811 | [Zen](https://github.com/s0md3v/Zen) | Finds email addresses of GitHub users. | 812 813 ## Sources 814 815 Both catalogues below are maintained by other people and are considerably larger than 816 this page. Use them as the canonical index; this sheet is a working route through them. 817 818 - [Bellingcat's Online Investigation Toolkit](https://bellingcat.gitbook.io/toolkit) — ~340 tools, each with its own 819 review page covering cost, difficulty, requirements and limitations. 820 - [OSINT Newsletter Tools Library](https://tools.osintnewsletter.com) — ~280 tools, organised by investigative goal. 821 822 Neither publishes a licence, so nothing here is copied from them: tool names, one-line 823 descriptions, cost flags and links are catalogue facts, and the method and commentary are 824 this site's own. See [credits](/credits).