daemon-sec-cheatsheet

The cheatsheet vault for operators: AD, enumeration, exploitation, priv-esc, web, DFIR
git clone https://git.daemon-sec.xyz/daemon-sec-cheatsheet.git
Log | Files | Refs | README | LICENSE

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).