diff --git a/.img/combined_enrichment_1.png b/.img/combined_enrichment_1.png new file mode 100644 index 0000000..f908cd2 Binary files /dev/null and b/.img/combined_enrichment_1.png differ diff --git a/.img/combined_enrichment_2.png b/.img/combined_enrichment_2.png new file mode 100644 index 0000000..db0c7f5 Binary files /dev/null and b/.img/combined_enrichment_2.png differ diff --git a/.img/company_enrichment.png b/.img/company_enrichment.png new file mode 100644 index 0000000..77b598c Binary files /dev/null and b/.img/company_enrichment.png differ diff --git a/.img/email_query_email_finder.png b/.img/email_query_email_finder.png new file mode 100644 index 0000000..d5624f2 Binary files /dev/null and b/.img/email_query_email_finder.png differ diff --git a/.img/email_verification.png b/.img/email_verification.png new file mode 100644 index 0000000..0ea4142 Binary files /dev/null and b/.img/email_verification.png differ diff --git a/.img/hunter_domain_search.png b/.img/hunter_domain_search.png new file mode 100644 index 0000000..5ad8c91 Binary files /dev/null and b/.img/hunter_domain_search.png differ diff --git a/.img/hunter_email_finder.png b/.img/hunter_email_finder.png new file mode 100644 index 0000000..9d7cc8e Binary files /dev/null and b/.img/hunter_email_finder.png differ diff --git a/.img/hunter_email_query.png b/.img/hunter_email_query.png new file mode 100644 index 0000000..e872178 Binary files /dev/null and b/.img/hunter_email_query.png differ diff --git a/.img/person_enrichment.png b/.img/person_enrichment.png new file mode 100644 index 0000000..dfce9e6 Binary files /dev/null and b/.img/person_enrichment.png differ diff --git a/README.md b/README.md index 9b654a9..ee46b8f 100644 --- a/README.md +++ b/README.md @@ -210,6 +210,71 @@ dehasher whois -d google.com -s --- +## 🌐 Hunter.io +Dehasher supports Hunter.io lookups. +Hunter.io lookups require a separate API Key from the Dehashed API. +This can be set using the `set-hunter` command. +```bash +# Set the Hunter.io API key +dehasher set-hunter +``` + +### Domain Search +Dehasher can perform a domain search for a given domain. +This provides a list of all emails that match the given query. +![Alt text](.img/hunter_domain_search.png "Hunter.io Domain Search") +```bash +# Perform a Hunter.io domain search for example.com +dehasher hunter -d example.com -D +``` + +### Email Finder +Dehasher can perform an email finder search for a given domain, first name, and last name. +This provides a list of all emails that match the given query. +![Alt text](.img/hunter_email_finder.png "Hunter.io Email Finder") +```bash +# Perform a Hunter.io email finder search for example.com +dehasher hunter -d example.com -F John -L Doe -E +``` + +### Email Verification +Dehasher can perform an email verification search for a given email. +This provides a list of all emails that match the given query. +![Alt text](.img/email_verification.png "Hunter.io Email Verification") +```bash +# Perform a Hunter.io email verification search for example@target.com +dehasher hunter -e example@target.com -V +``` + +### Company Enrichment +Dehasher can perform a company enrichment search for a given domain. +This provides a list of all emails that match the given query. +![Alt text](.img/company_enrichment.png "Hunter.io Company Enrichment") +```bash +# Perform a Hunter.io company enrichment search for example.com +dehasher hunter -d example.com -C +``` + +### Person Enrichment +Dehasher can perform a person enrichment search for a given email. +This provides a list of all emails that match the given query. +![Alt text](.img/person_enrichment.png "Hunter.io Person Enrichment") +```bash +# Perform a Hunter.io person enrichment search for example@target.com +dehasher hunter -e example@target.com -P +``` + +### Combined Enrichment +Dehasher can perform a combined enrichment search for a given email. +This provides a list of all emails that match the given query. +![Alt text](.img/combined_enrichment_1.png "Hunter.io Combined Enrichment") +![Alt text](.img/combined_enrichment_2.png "Hunter.io Combined Enrichment") +```bash +# Perform a Hunter.io combined enrichment search for example@target.com +dehasher hunter -e example@target.com -B +``` + +--- ## 📊 Database Querying Dehasher stores query results in a local database. This database can be queried for previous results. @@ -267,6 +332,10 @@ The current tables available for query are: - Previous query runs to the dehashed API - lookup - Results of any Whois NS, MX, or IP lookup +- hunter_domain + - Results from a hunter.io domain search +- hunter_email + - Results extracted from a domain saerch and email finder. --- diff --git a/cmd/api.go b/cmd/api.go index d4e0939..0e84c98 100644 --- a/cmd/api.go +++ b/cmd/api.go @@ -11,7 +11,7 @@ import ( ) func init() { - // Add query command to root command + // Add api command to root command rootCmd.AddCommand(apiCmd) // Add flags specific to api command @@ -25,7 +25,7 @@ func init() { apiCmd.Flags().StringVarP(&outputFormat, "format", "f", "json", "Output format (json, yaml, xml, txt)") apiCmd.Flags().StringVarP(&outputFile, "output", "o", "query", "File to output results to including extension") apiCmd.Flags().StringVarP(&usernameQuery, "username", "U", "", "Username query") - apiCmd.Flags().StringVarP(&emailQuery, "email-query", "E", "", "Email query") + apiCmd.Flags().StringVarP(&emailQuery, "email-query", "E", "", "HunterEmail query") apiCmd.Flags().StringVarP(&ipQuery, "ip", "I", "", "IP address query") apiCmd.Flags().StringVarP(&domainQuery, "domain", "D", "", "Domain query") apiCmd.Flags().StringVarP(&passwordQuery, "password", "P", "", "Password query") @@ -73,7 +73,7 @@ var ( Short: "Query the Dehashed API", Long: `Query the Dehashed API for emails, usernames, passwords, hashes, IP addresses, and names.`, Run: func(cmd *cobra.Command, args []string) { - key := getStoredApiKey() + key := getDehashedApiKey() // Validate credentials if key == "" { @@ -135,6 +135,6 @@ var ( ) // Helper functions to get stored API credentials -func getStoredApiKey() string { - return badger.GetKey() +func getDehashedApiKey() string { + return badger.GetDehashedKey() } diff --git a/cmd/hunter.go b/cmd/hunter.go new file mode 100644 index 0000000..ae0c715 --- /dev/null +++ b/cmd/hunter.go @@ -0,0 +1,429 @@ +package cmd + +import ( + "dehasher/internal/badger" + "dehasher/internal/debug" + "dehasher/internal/export" + "dehasher/internal/files" + hunter "dehasher/internal/hunter.io" + "dehasher/internal/pretty" + "fmt" + "github.com/spf13/cobra" + "go.uber.org/zap" + "time" +) + +func init() { + // Add hunter command to root command + rootCmd.AddCommand(hunterCmd) + + // Add flags specific to hunter command + hunterCmd.Flags().StringVarP(&hunterDomain, "domain", "d", "", "Domain to query") + hunterCmd.Flags().StringVarP(&hunterEmail, "email", "e", "", "Email to query") + hunterCmd.Flags().StringVarP(&hunterFirstName, "first-name", "F", "", "First name to query") + hunterCmd.Flags().StringVarP(&hunterLastName, "last-name", "L", "", "Last name to query") + hunterCmd.Flags().BoolVarP(&hunterDomainSearch, "domain-search", "D", false, "Search for domain") + hunterCmd.Flags().BoolVarP(&hunterEmailFind, "email-find", "E", false, "Find emails for user using domain, first name, and last name") + hunterCmd.Flags().BoolVarP(&hunterEmailVerify, "email-verify", "V", false, "Verify email") + hunterCmd.Flags().BoolVarP(&hunterCompanyEnrichmentDomain, "company-enrichment", "C", false, "Company enrichment for domain") + hunterCmd.Flags().BoolVarP(&hunterPersonEnrichmentEmail, "person-enrichment", "P", false, "Person enrichment for email") + hunterCmd.Flags().BoolVarP(&hunterCombinedEnrichmentEmail, "combined-enrichment", "B", false, "Combined Company and Person enrichment for email") + hunterCmd.Flags().StringVarP(&hunterOutputFormat, "format", "f", "json", "Output format (json, yaml, xml, txt)") + hunterCmd.Flags().StringVarP(&hunterOutputFile, "output", "o", "hunter", "File to output results to including extension") + + // Add mutually exclusive flags to hunter command + hunterCmd.MarkFlagsMutuallyExclusive("email-find") + +} + +var ( + // Hunter Commands Flags + hunterDomain string + hunterEmail string + hunterFirstName string + hunterLastName string + hunterDomainSearch bool + hunterEmailFind bool + hunterEmailVerify bool + hunterCompanyEnrichmentDomain bool + hunterPersonEnrichmentEmail bool + hunterCombinedEnrichmentEmail bool + hunterOutputFormat string + hunterOutputFile string + + hunterCmd = &cobra.Command{ + Use: "hunter", + Short: "Hunter.io API interaction", + Long: `Interact with the Hunter.io API for email and domain information.`, + Run: func(cmd *cobra.Command, args []string) { + if debugGlobal { + debug.PrintInfo("debug mode enabled") + zap.L().Info("hunter_debug", + zap.String("message", "debug mode enabled"), + ) + } + + // Flag Checks + if !hunterFlagCheck() { + return + } + + if hunterOutputFile == "" { + if debugGlobal { + debug.PrintInfo("output file not specified, using default") + } + hunterOutputFile = "hunter_" + time.Now().Format("05_04_05") + } + + if hunterOutputFormat == "" { + if debugGlobal { + debug.PrintInfo("output format not specified, using default") + } + hunterOutputFormat = "json" + } + + fType := files.GetFileType(hunterOutputFormat) + if fType == files.UNKNOWN { + fmt.Println("[!] Error: Invalid output format. Must be 'json', 'xml', 'yaml', or 'txt'.") + return + } + if debugGlobal { + debug.PrintInfo("using output format: " + hunterOutputFormat) + } + + fmt.Println("[*] Hunter.io API interaction [Beta]") + + h := hunter.NewHunterIO(getHunterApiKey(), debugGlobal) + + if hunterDomainSearch { + fmt.Println("[*] Performing domain search search...") + result, err := h.DomainSearch(hunterDomain) + if err != nil { + if debugGlobal { + debug.PrintInfo("failed to perform domain search") + debug.PrintError(err) + } + zap.L().Error("hunter_domain_search", + zap.String("message", "failed to perform domain search"), + zap.Error(err), + ) + fmt.Printf("Error performing domain search: %v\n", err) + return + } + + // Write Hunter.io Domain Search Result to file + fmt.Printf("[*] Writing Hunter.io Domain Search Result to file: %s%s\n", hunterOutputFile, fType.Extension()) + err = export.WriteHunterDomainToFile(result, hunterOutputFile, fType) + if err != nil { + if debugGlobal { + debug.PrintInfo("failed to write hunter domain search to file") + debug.PrintError(err) + } + zap.L().Error("write_hunter_domain_search", + zap.String("message", "failed to write hunter domain search to file"), + zap.Error(err), + ) + fmt.Printf("Error writing Hunter.io Domain Search Result to file: %v\n", err) + } + + // Pretty Print Hunter.io Domain Search Result + fmt.Println("Domain Search Result:") + pretty.HunterDomainTree(hunterDomain, result) + return + } + + if hunterEmailFind { + fmt.Println("[*] Performing email find search...") + result, err := h.EmailFinder(hunterDomain, hunterFirstName, hunterLastName) + if err != nil { + if debugGlobal { + debug.PrintInfo("failed to perform email find") + debug.PrintError(err) + } + zap.L().Error("hunter_email_find", + zap.String("message", "failed to perform email find"), + zap.Error(err), + ) + fmt.Printf("Error performing email find: %v\n", err) + return + } + + // Write Hunter.io Email Finder Result to file + fmt.Printf("[*] Writing Hunter.io Email Finder Result to file: %s%s\n", hunterOutputFile, fType.Extension()) + err = export.WriteHunterEmailToFile(result, hunterOutputFile, fType) + if err != nil { + if debugGlobal { + debug.PrintInfo("failed to write hunter email find to file") + debug.PrintError(err) + } + zap.L().Error("write_hunter_email_find", + zap.String("message", "failed to write hunter email find to file"), + zap.Error(err), + ) + fmt.Printf("Error writing Hunter.io Email Finder Result to file: %v\n", err) + } + + fmt.Println("Email Find Result:") + + var ( + headers = []string{"Email", "Score", "Domain", "Accept All", "Position", "Twitter", "Linkedin", "Phone Number", "Company", "Sources", "Verification"} + rows [][]string + ) + + rows = append(rows, []string{ + result.Email, + fmt.Sprintf("%d", result.Score), + result.Domain, + fmt.Sprintf("%t", result.AcceptAll), + result.Position, + result.Twitter, + result.LinkedinURL, + result.PhoneNumber, + result.Company, + fmt.Sprintf("%v", result.Sources), + fmt.Sprintf("%v", result.Verification), + }) + + pretty.Table(headers, rows) + + return + } + + if hunterEmailVerify { + fmt.Println("[*] Performing email verification search...") + result, err := h.EmailVerification(hunterEmail) + if err != nil { + if debugGlobal { + debug.PrintInfo("failed to perform email verification") + debug.PrintError(err) + } + zap.L().Error("hunter_email_verification", + zap.String("message", "failed to perform email verification"), + zap.Error(err), + ) + fmt.Printf("Error performing email verification: %v\n", err) + return + } + // Write Hunter.io Email Verification Result to file + fmt.Printf("[*] Writing Hunter.io Email Verification Result to file: %s%s\n", hunterOutputFile, fType.Extension()) + err = export.WriteHunterEmailVerifyToFile(result, hunterOutputFile, fType) + if err != nil { + if debugGlobal { + debug.PrintInfo("failed to write hunter email verification to file") + debug.PrintError(err) + } + zap.L().Error("write_hunter_email_verification", + zap.String("message", "failed to write hunter email verification to file"), + zap.Error(err), + ) + fmt.Printf("Error writing Hunter.io Email Verification Result to file: %v\n", err) + } + + // Pretty Print Hunter.io Email Verification Result + var ( + headers = []string{"Email", "Status", "Result", "Score", "Regexp", "Gibberish", "Disposable", "Webmail", "MX Records", "SMTP Server", "SMTP Check", "Accept All", "Block", "Sources"} + rows [][]string + ) + rows = append(rows, []string{ + result.Email, + result.Status, + result.Result, + fmt.Sprintf("%d", result.Score), + fmt.Sprintf("%t", result.Regexp), + fmt.Sprintf("%t", result.Gibberish), + fmt.Sprintf("%t", result.Disposable), + fmt.Sprintf("%t", result.Webmail), + fmt.Sprintf("%t", result.MXRecords), + fmt.Sprintf("%t", result.SMTPServer), + fmt.Sprintf("%t", result.SMTPCheck), + fmt.Sprintf("%t", result.AcceptAll), + fmt.Sprintf("%t", result.Block), + fmt.Sprintf("%v", result.Sources), + }) + + fmt.Println("Email Verification Result:") + pretty.Table(headers, rows) + return + } + + if hunterCompanyEnrichmentDomain { + fmt.Println("[*] Performing company enrichment search...") + result, err := h.CompanyEnrichment(hunterDomain) + if err != nil { + if debugGlobal { + debug.PrintInfo("failed to perform company enrichment") + debug.PrintError(err) + } + zap.L().Error("hunter_company_enrichment", + zap.String("message", "failed to perform company enrichment"), + zap.Error(err), + ) + fmt.Printf("Error performing company enrichment: %v\n", err) + return + } + + // Write to file + fmt.Printf("[*] Writing Hunter.io Company Enrichment Result to file: %s%s\n", hunterOutputFile, fType.Extension()) + err = export.WriteHunterCompanyEnrichmentToFile(result, hunterOutputFile, fType) + if err != nil { + if debugGlobal { + debug.PrintInfo("failed to write hunter company enrichment to file") + debug.PrintError(err) + } + zap.L().Error("write_hunter_company_enrichment", + zap.String("message", "failed to write hunter company enrichment to file"), + zap.Error(err), + ) + fmt.Printf("Error writing Hunter.io Company Enrichment Result to file: %v\n", err) + } + + // Pretty Print Hunter.io Company Enrichment Result + fmt.Println("Company Enrichment Result:") + pretty.HunterCompanyEnrichmentTree(hunterDomain, result) + + return + } + + if hunterPersonEnrichmentEmail { + fmt.Println("[*] Performing person enrichment search...") + result, err := h.PersonEnrichment(hunterEmail) + if err != nil { + if debugGlobal { + debug.PrintInfo("failed to perform person enrichment") + debug.PrintError(err) + } + zap.L().Error("hunter_person_enrichment", + zap.String("message", "failed to perform person enrichment"), + zap.Error(err), + ) + fmt.Printf("Error performing person enrichment: %v\n", err) + return + } + + // Write to file + fmt.Printf("[*] Writing Hunter.io Person Enrichment Result to file: %s%s\n", hunterOutputFile, fType.Extension()) + err = export.WriteHunterPersonEnrichmentToFile(result, hunterOutputFile, fType) + if err != nil { + if debugGlobal { + debug.PrintInfo("failed to write hunter person enrichment to file") + debug.PrintError(err) + } + zap.L().Error("write_hunter_person_enrichment", + zap.String("message", "failed to write hunter person enrichment to file"), + zap.Error(err), + ) + fmt.Printf("Error writing Hunter.io Person Enrichment Result to file: %v\n", err) + } + + // Pretty Print Hunter.io Person Enrichment Result + fmt.Println("Person Enrichment Result:") + pretty.HunterPersonEnrichmentTree(hunterEmail, result) + return + } + + if hunterCombinedEnrichmentEmail { + fmt.Println("[*] Performing combined enrichment search...") + result, err := h.CombinedEnrichment(hunterEmail) + if err != nil { + if debugGlobal { + debug.PrintInfo("failed to perform combined enrichment") + debug.PrintError(err) + } + zap.L().Error("hunter_combined_enrichment", + zap.String("message", "failed to perform combined enrichment"), + zap.Error(err), + ) + fmt.Printf("Error performing combined enrichment: %v\n", err) + return + } + + // Write to file + fmt.Printf("[*] Writing Hunter.io Combined Enrichment Result to file: %s%s\n", hunterOutputFile, fType.Extension()) + err = export.WriteHunterCombinedEnrichmentToFile(result, hunterOutputFile, fType) + if err != nil { + if debugGlobal { + debug.PrintInfo("failed to write hunter combined enrichment to file") + debug.PrintError(err) + } + zap.L().Error("write_hunter_combined_enrichment", + zap.String("message", "failed to write hunter combined enrichment to file"), + zap.Error(err), + ) + fmt.Printf("Error writing Hunter.io Combined Enrichment Result to file: %v\n", err) + } + + fmt.Println("Combined Enrichment Result:") + pretty.HunterCombinedEnrichmentTree(hunterEmail, result) + return + } + + }, + } +) + +func hunterFlagCheck() bool { + if debugGlobal { + debug.PrintInfo("checking flags") + } + + var optionSet bool + + if hunterDomainSearch { + if hunterDomain == "" { + fmt.Println("Domain is required for domain search") + return false + } + optionSet = true + } + if hunterEmailVerify { + if hunterEmail == "" { + fmt.Println("Email is required for email verification") + return false + } + optionSet = true + } + if hunterCompanyEnrichmentDomain { + if hunterDomain == "" { + fmt.Println("Domain is required for company enrichment") + return false + } + optionSet = true + } + if hunterPersonEnrichmentEmail { + if hunterEmail == "" { + fmt.Println("Email is required for person enrichment") + return false + } + optionSet = true + } + if hunterCombinedEnrichmentEmail { + if hunterEmail == "" { + fmt.Println("Email is required for combined enrichment") + return false + } + optionSet = true + } + if hunterEmailFind { + if hunterFirstName == "" || hunterLastName == "" { + fmt.Println("First name and last name are required for email find") + return false + } + if hunterDomain == "" { + fmt.Println("Domain is required for email find") + return false + } + optionSet = true + } + + if !optionSet { + fmt.Println("[!] No options selected") + return false + } + + return true +} + +// Helper functions to get stored API credentials +func getHunterApiKey() string { + return badger.GetHunterKey() +} diff --git a/cmd/query.go b/cmd/query.go index bf5f4b3..16ad8c7 100644 --- a/cmd/query.go +++ b/cmd/query.go @@ -44,6 +44,15 @@ var availableTables = map[string][]string{ "name_servers", "parse_code", "raw_text", "registrant", "registrar_iana_id", "registrar_name", "registry_data", "status", "stripped_text", "updated_date", "updated_date_normalized", }, + "hunter_domain": { + "id", "created_at", "updated_at", "deleted_at", "domain", "disposable", "webmail", "accept_all", "pattern", + "organization", "description", "industry", "twitter", "facebook", "linkedin", "instagram", "youtube", + "technologies", "country", "state", "city", "postal_code", "street", "headcount", "company_type", "emails", "linked_domains", + }, + "hunter_email": { + "id", "created_at", "updated_at", "deleted_at", "value", "type", "confidence", "sources", "first_name", "last_name", + "position", "position_raw", "seniority", "department", "linkedin", "twitter", "phone_number", "verification_date", "verification_status", + }, } // Function to list available tables and their columns diff --git a/cmd/root.go b/cmd/root.go index 706b259..5d80bd8 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -66,21 +66,38 @@ func init() { rootCmd.PersistentFlags().BoolVar(&debugGlobal, "debug", false, "Show debug information") // Add subcommands - rootCmd.AddCommand(setKeyCmd) + rootCmd.AddCommand(setDehashedKeyCmd) + rootCmd.AddCommand(setHunterKeyCmd) rootCmd.AddCommand(setLocalDb) } // Command to set API key -var setKeyCmd = &cobra.Command{ - Use: "set-key [key]", - Short: "Set and store API key", +var setDehashedKeyCmd = &cobra.Command{ + Use: "set-dehashed [key]", + Short: "Set and store Dehashed.com API key", Args: cobra.ExactArgs(1), Run: func(cmd *cobra.Command, args []string) { key := args[0] // Store key in badger DB - err := storeApiKey(key) + err := storeDehashedApiKey(key) if err != nil { - fmt.Printf("Error storing API key: %v\n", err) + fmt.Printf("Error storing Dehashed API key: %v\n", err) + return + } + fmt.Println("API key stored successfully") + }, +} + +var setHunterKeyCmd = &cobra.Command{ + Use: "set-hunter [key]", + Short: "Set and store Hunter.io API key", + Args: cobra.ExactArgs(1), + Run: func(cmd *cobra.Command, args []string) { + key := args[0] + // Store key in badger DB + err := storeHunterApiKey(key) + if err != nil { + fmt.Printf("Error storing Hunter API key: %v\n", err) return } fmt.Println("API key stored successfully") @@ -116,8 +133,17 @@ var setLocalDb = &cobra.Command{ } // Helper functions to store API credentials -func storeApiKey(key string) error { - err := badger.StoreKey(key) +func storeDehashedApiKey(key string) error { + err := badger.StoreDehashedKey(key) + if err != nil { + fmt.Printf("Error storing API key: %v\n", err) + return err + } + return nil +} + +func storeHunterApiKey(key string) error { + err := badger.StoreHunterKey(key) if err != nil { fmt.Printf("Error storing API key: %v\n", err) return err diff --git a/cmd/whois.go b/cmd/whois.go index b3b5dd8..eda1b04 100644 --- a/cmd/whois.go +++ b/cmd/whois.go @@ -55,7 +55,7 @@ var ( Short: "Dehashed WHOIS lookups and reverse WHOIS searches", Long: `Perform WHOIS lookups, history searches, reverse WHOIS searches, IP lookups, MX lookups, NS lookups, and subdomain scans.`, Run: func(cmd *cobra.Command, args []string) { - key := getStoredApiKey() + key := getDehashedApiKey() // Validate credentials if key == "" { @@ -172,6 +172,7 @@ var ( } if whoisHistory { + filename := whoisOutputFile + "_history" fmt.Println("[*] Performing WHOIS history search...") // Perform history search historyRecords, err := w.WhoisHistory(whoisDomain) @@ -194,7 +195,7 @@ var ( if len(historyRecords) > 0 { fmt.Println("[*] Records Found: %d\n", len(historyRecords)) fmt.Println("[*] WHOIS History being written to file: %s%s\n", whoisOutputFile, fType.Extension()) - writeErr := export.WriteWhoIsHistoryToFile(historyRecords, whoisOutputFile, fType) + writeErr := export.WriteWhoIsHistoryToFile(historyRecords, filename, fType) if writeErr != nil { if debugGlobal { debug.PrintInfo("failed to write whois history to file") @@ -233,6 +234,7 @@ var ( // Perform subdomain scan if whoisSubdomainScan { + filename := whoisOutputFile + "_subdomains" fmt.Println("[*] Performing WHOIS subdomain scan...") subdomains, err := w.WhoisSubdomainScan(whoisDomain) @@ -268,7 +270,7 @@ var ( // Write the subdomains to file if any if len(subdomains) > 0 { fmt.Printf("[*] Writing subdomains to file: %s%s\n", whoisOutputFile, fType.Extension()) - err = export.WriteSubdomainsToFile(subdomains, whoisOutputFile, fType) + err = export.WriteSubdomainsToFile(subdomains, filename, fType) if err != nil { zap.L().Error("write_whois_subdomain", zap.String("message", "failed to write whois subdomain to file"), diff --git a/internal/badger/badger.go b/internal/badger/badger.go index 3950a55..beeb042 100644 --- a/internal/badger/badger.go +++ b/internal/badger/badger.go @@ -100,7 +100,7 @@ func Close() { } } -func GetKey() string { +func GetDehashedKey() string { var apiKey string err := db.View(func(txn *badger.Txn) error { @@ -124,6 +124,29 @@ func GetKey() string { return apiKey } +func GetHunterKey() string { + var apiKey string + + err := db.View(func(txn *badger.Txn) error { + item, err := txn.Get([]byte("cfg:hunter_api_key")) + if err != nil { + return err // could be ErrKeyNotFound + } + return item.Value(func(val []byte) error { + apiKey = string(val) + return nil + }) + }) + + if err != nil { + zap.L().Error("get_hunter_api_key", + zap.String("message", "failed to get hunter_api_key"), + zap.Error(err), + ) + } + return apiKey +} + func GetUseLocalDB() bool { var useLocal bool @@ -162,13 +185,26 @@ func GetUseLocalDB() bool { return useLocal } -func StoreKey(apiKey string) error { +func StoreDehashedKey(apiKey string) error { err := db.Update(func(txn *badger.Txn) error { return txn.Set([]byte("cfg:api_key"), []byte(apiKey)) }) if err != nil { zap.L().Error("set_api_key", - zap.String("message", "failed to set api_key"), + zap.String("message", "failed to set dehashed api_key"), + zap.Error(err), + ) + } + return err +} + +func StoreHunterKey(apiKey string) error { + err := db.Update(func(txn *badger.Txn) error { + return txn.Set([]byte("cfg:hunter_api_key"), []byte(apiKey)) + }) + if err != nil { + zap.L().Error("set_api_key", + zap.String("message", "failed to set hunter api_key"), zap.Error(err), ) } diff --git a/internal/export/hunter.go b/internal/export/hunter.go new file mode 100644 index 0000000..99d093e --- /dev/null +++ b/internal/export/hunter.go @@ -0,0 +1,161 @@ +package export + +import ( + "dehasher/internal/files" + "dehasher/internal/sqlite" + "encoding/json" + "encoding/xml" + "fmt" + "gopkg.in/yaml.v3" + "os" +) + +func WriteHunterDomainToFile(result sqlite.HunterDomainData, outputFile string, fileType files.FileType) error { + var data []byte + var err error + + switch fileType { + case files.JSON: + data, err = json.MarshalIndent(result, "", " ") + case files.XML: + data, err = xml.MarshalIndent(result, "", " ") + case files.YAML: + data, err = yaml.Marshal(result) + case files.TEXT: + data = []byte(result.String()) + default: + return err + } + + if err != nil { + return err + } + + filePath := fmt.Sprintf("%s.%s", outputFile, fileType.String()) + return os.WriteFile(filePath, data, 0644) +} + +func WriteHunterEmailToFile(result sqlite.HunterEmailFinderData, outputFile string, fileType files.FileType) error { + var data []byte + var err error + + switch fileType { + case files.JSON: + data, err = json.MarshalIndent(result, "", " ") + case files.XML: + data, err = xml.MarshalIndent(result, "", " ") + case files.YAML: + data, err = yaml.Marshal(result) + case files.TEXT: + data = []byte(result.String()) + default: + return err + } + + if err != nil { + return err + } + + filePath := fmt.Sprintf("%s.%s", outputFile, fileType.String()) + return os.WriteFile(filePath, data, 0644) +} + +func WriteHunterEmailVerifyToFile(result sqlite.HunterEmailVerifyData, outputFile string, fileType files.FileType) error { + var data []byte + var err error + + switch fileType { + case files.JSON: + data, err = json.MarshalIndent(result, "", " ") + case files.XML: + data, err = xml.MarshalIndent(result, "", " ") + case files.YAML: + data, err = yaml.Marshal(result) + case files.TEXT: + data = []byte(result.String()) + default: + return err + } + + if err != nil { + return err + } + + filePath := fmt.Sprintf("%s.%s", outputFile, fileType.String()) + return os.WriteFile(filePath, data, 0644) +} + +func WriteHunterCompanyEnrichmentToFile(result sqlite.CompanyData, outputFile string, fileType files.FileType) error { + var data []byte + var err error + + switch fileType { + case files.JSON: + data, err = json.MarshalIndent(result, "", " ") + case files.XML: + data, err = xml.MarshalIndent(result, "", " ") + case files.YAML: + data, err = yaml.Marshal(result) + case files.TEXT: + data = []byte(result.String()) + default: + return err + } + + if err != nil { + return err + } + + filePath := fmt.Sprintf("%s.%s", outputFile, fileType.String()) + return os.WriteFile(filePath, data, 0644) +} + +func WriteHunterPersonEnrichmentToFile(result sqlite.PersonData, outputFile string, fileType files.FileType) error { + var data []byte + var err error + + switch fileType { + case files.JSON: + data, err = json.MarshalIndent(result, "", " ") + case files.XML: + data, err = xml.MarshalIndent(result, "", " ") + case files.YAML: + data, err = yaml.Marshal(result) + case files.TEXT: + data = []byte(result.String()) + default: + return err + } + + if err != nil { + return err + } + + filePath := fmt.Sprintf("%s.%s", outputFile, fileType.String()) + return os.WriteFile(filePath, data, 0644) +} + +func WriteHunterCombinedEnrichmentToFile(result sqlite.CombinedData, outputFile string, fileType files.FileType) error { + var data []byte + var err error + + switch fileType { + case files.JSON: + data, err = json.MarshalIndent(result, "", " ") + case files.XML: + data, err = xml.MarshalIndent(result, "", " ") + case files.YAML: + data, err = yaml.Marshal(result) + case files.TEXT: + data = []byte(result.String()) + default: + return err + } + + if err != nil { + return err + } + + filePath := fmt.Sprintf("%s.%s", outputFile, fileType.String()) + return os.WriteFile(filePath, data, 0644) +} diff --git a/internal/hunter.io/hunter.io.go b/internal/hunter.io/hunter.io.go new file mode 100644 index 0000000..b933779 --- /dev/null +++ b/internal/hunter.io/hunter.io.go @@ -0,0 +1,696 @@ +package hunter_io + +import ( + "dehasher/internal/debug" + "dehasher/internal/sqlite" + "encoding/json" + "fmt" + "go.uber.org/zap" + "io" + "net/http" + "strings" +) + +const ( + DOMAIN_SEARCH = "https://api.hunter.io/v2/domain-search?domain={{domain}}&api_key={{apikey}}" + EMAIL_FINDER = "https://api.hunter.io/v2/email-finder?domain={{domain}}&first_name={{first_name}}&last_name={{last_name}}&api_key={{apikey}}" + EMAIL_VERIFICATION = "https://api.hunter.io/v2/email-verifier?email={{email}}&api_key={{apikey}}" + COMPANY_ENRICHMENT = "https://api.hunter.io/v2/companies/find?domain={{domain}}&api_key={{apikey}}" + PERSON_ENRICHMENT = "https://api.hunter.io/v2/people/find?email={{email}}&api_key={{apikey}}" + COMBINED_ENRICHMENT = "https://api.hunter.io/v2/combined/find?email={{email}}&api_key={{apikey}}" +) + +type HunterIO struct { + apiKey string + debug bool +} + +func NewHunterIO(apiKey string, debugEnabled bool) *HunterIO { + return &HunterIO{apiKey: apiKey, debug: debugEnabled} +} + +func (h *HunterIO) DomainSearch(domain string) (sqlite.HunterDomainData, error) { + var hunterDomainData sqlite.HunterDomainData + + if h.debug { + debug.PrintInfo("performing domain search") + zap.L().Info("hunter_domain_search_debug", + zap.String("message", "performing domain search"), + ) + } + + url := DOMAIN_SEARCH + url = strings.Replace(url, "{{domain}}", domain, -1) + url = strings.Replace(url, "{{apikey}}", h.apiKey, -1) + + if h.debug { + debug.PrintInfo("performing request") + debug.PrintInfo(fmt.Sprintf("URL: %s\n", url)) + zap.L().Info("hunter_domain_search_debug", + zap.String("message", "performing request"), + zap.String("url", url), + ) + } + + resp, err := http.Get(url) + if err != nil { + if h.debug { + debug.PrintInfo("failed to perform request") + debug.PrintError(err) + } + zap.L().Error("hunter_domain_search", + zap.String("message", "failed to perform request"), + zap.Error(err), + ) + return hunterDomainData, err + } + defer resp.Body.Close() + + b, err := io.ReadAll(resp.Body) + if err != nil { + if h.debug { + debug.PrintInfo("failed to read response body") + debug.PrintError(err) + } + zap.L().Error("hunter_domain_search", + zap.String("message", "failed to read response body"), + zap.Error(err), + ) + return hunterDomainData, err + } + + if resp.StatusCode != 200 { + if h.debug { + debug.PrintInfo("received error status code") + debug.PrintJson(fmt.Sprintf("Status Code: %d\n", resp.StatusCode)) + debug.PrintJson(fmt.Sprintf("Body: %s\n", string(b))) + zap.L().Info("hunter_domain_search_debug", + zap.String("message", "received error status code"), + zap.Int("status_code", resp.StatusCode), + zap.String("body_error", string(b)), + ) + + } + zap.L().Error("hunter_domain_search", + zap.String("message", "received error status code"), + zap.Int("status_code", resp.StatusCode), + zap.String("body_error", string(b)), + ) + return hunterDomainData, fmt.Errorf("received error status code: %d", resp.StatusCode) + } + + if h.debug { + debug.PrintInfo("unmarshalled response body") + debug.PrintJson(fmt.Sprintf("Body: %s\n", string(b))) + zap.L().Info("hunter_domain_search_debug", + zap.String("message", "unmarshalled response body"), + zap.String("body", string(b)), + ) + } + + var hunterDomainSearchResult sqlite.HunterDomainSearchResult + err = json.Unmarshal(b, &hunterDomainSearchResult) + if err != nil { + if h.debug { + debug.PrintInfo("failed to unmarshal response body") + debug.PrintError(err) + } + zap.L().Error("hunter_domain_search", + zap.String("message", "failed to unmarshal response body"), + zap.Error(err), + ) + return hunterDomainData, err + } + + hunterDomainData = hunterDomainSearchResult.Data + + // Create a list of email object associated with the domain + var emails []sqlite.HunterEmail + for _, email := range hunterDomainData.Emails { + emails = append(emails, sqlite.HunterEmail{ + Domain: domain, + Value: email.Value, + Type: email.Type, + Confidence: email.Confidence, + Sources: email.Sources, + FirstName: email.FirstName, + LastName: email.LastName, + Position: email.Position, + PositionRaw: email.PositionRaw, + Seniority: email.Seniority, + Department: email.Department, + Linkedin: email.Linkedin, + Twitter: email.Twitter, + PhoneNumber: email.PhoneNumber, + Verification: email.Verification, + }) + } + + err = sqlite.StoreHunterEmails(emails) + if err != nil { + if h.debug { + debug.PrintInfo("failed to store hunter emails") + debug.PrintError(err) + } + zap.L().Error("store_hunter_emails", + zap.String("message", "failed to store hunter emails"), + zap.Error(err), + ) + return hunterDomainData, err + } + + err = sqlite.StoreHunterDomain(hunterDomainData) + if err != nil { + if h.debug { + debug.PrintInfo("failed to store hunter domain") + debug.PrintError(err) + } + zap.L().Error("store_hunter_domain", + zap.String("message", "failed to store hunter domain"), + zap.Error(err), + ) + return hunterDomainData, err + } + + return hunterDomainData, nil +} + +func (h *HunterIO) EmailFinder(domain, firstName, lastName string) (sqlite.HunterEmailFinderData, error) { + var hunterEmailFinderData sqlite.HunterEmailFinderData + + if h.debug { + debug.PrintInfo("performing email find") + zap.L().Info("hunter_email_find_debug", + zap.String("message", "performing email find"), + ) + } + + url := EMAIL_FINDER + url = strings.Replace(url, "{{domain}}", domain, -1) + url = strings.Replace(url, "{{first_name}}", firstName, -1) + url = strings.Replace(url, "{{last_name}}", lastName, -1) + url = strings.Replace(url, "{{apikey}}", h.apiKey, -1) + + if h.debug { + debug.PrintInfo("performing request") + debug.PrintInfo(fmt.Sprintf("URL: %s\n", url)) + zap.L().Info("hunter_email_find_debug", + zap.String("message", "performing request"), + zap.String("url", url), + ) + } + + resp, err := http.Get(url) + if err != nil { + if h.debug { + debug.PrintInfo("failed to perform request") + debug.PrintError(err) + } + zap.L().Error("hunter_email_find", + zap.String("message", "failed to perform request"), + zap.Error(err), + ) + return hunterEmailFinderData, err + } + defer resp.Body.Close() + + b, err := io.ReadAll(resp.Body) + if err != nil { + if h.debug { + debug.PrintInfo("failed to read response body") + debug.PrintError(err) + } + zap.L().Error("hunter_email_find", + zap.String("message", "failed to read response body"), + zap.Error(err), + ) + return hunterEmailFinderData, err + } + + if resp.StatusCode != 200 { + if h.debug { + debug.PrintInfo("received error status code") + debug.PrintJson(fmt.Sprintf("Status Code: %d\n", resp.StatusCode)) + debug.PrintJson(fmt.Sprintf("Body: %s\n", string(b))) + zap.L().Info("hunter_email_find_debug", + zap.String("message", "received error status code"), + zap.Int("status_code", resp.StatusCode), + zap.String("body_error", string(b)), + ) + } + zap.L().Error("hunter_email_find", + zap.String("message", "received error status code"), + zap.Int("status_code", resp.StatusCode), + zap.String("body_error", string(b)), + ) + return hunterEmailFinderData, fmt.Errorf("received error status code: %d", resp.StatusCode) + } + + if h.debug { + debug.PrintInfo("unmarshalled response body") + debug.PrintJson(fmt.Sprintf("Body: %s\n", string(b))) + zap.L().Info("hunter_email_find_debug", + zap.String("message", "unmarshalled response body"), + zap.String("body", string(b)), + ) + } + + var hunterEmailFinderResult sqlite.HunterEmailFinderResponse + err = json.Unmarshal(b, &hunterEmailFinderResult) + if err != nil { + if h.debug { + debug.PrintInfo("failed to unmarshal response body") + debug.PrintError(err) + } + zap.L().Error("hunter_email_find", + zap.String("message", "failed to unmarshal response body"), + zap.Error(err), + ) + return hunterEmailFinderData, err + } + + hunterEmailFinderData = hunterEmailFinderResult.Data + + var hunterEmails []sqlite.HunterEmail + + hunterEmails = append(hunterEmails, sqlite.HunterEmail{ + Domain: hunterEmailFinderData.Domain, + Value: hunterEmailFinderData.Email, + Type: "personal", + Confidence: 100, + Sources: hunterEmailFinderData.Sources, + FirstName: hunterEmailFinderData.FirstName, + LastName: hunterEmailFinderData.LastName, + Position: hunterEmailFinderData.Position, + PositionRaw: "", + Seniority: "", + Department: "", + Linkedin: hunterEmailFinderData.LinkedinURL, + Twitter: hunterEmailFinderData.Twitter, + PhoneNumber: hunterEmailFinderData.PhoneNumber, + Verification: hunterEmailFinderData.Verification, + }) + + err = sqlite.StoreHunterEmails(hunterEmails) + if err != nil { + if h.debug { + debug.PrintInfo("failed to store hunter email finder") + debug.PrintError(err) + } + zap.L().Error("store_hunter_email_finder", + zap.String("message", "failed to store hunter email finder"), + zap.Error(err), + ) + return hunterEmailFinderData, err + } + + return hunterEmailFinderData, nil +} + +func (h *HunterIO) EmailVerification(email string) (sqlite.HunterEmailVerifyData, error) { + var hunterEmailVerifyData sqlite.HunterEmailVerifyData + + if h.debug { + debug.PrintInfo("performing email verification") + zap.L().Info("hunter_email_verification_debug", + zap.String("message", "performing email verification"), + ) + } + + url := EMAIL_VERIFICATION + url = strings.Replace(url, "{{email}}", email, -1) + url = strings.Replace(url, "{{apikey}}", h.apiKey, -1) + + if h.debug { + debug.PrintInfo("performing request") + debug.PrintInfo(fmt.Sprintf("URL: %s\n", url)) + zap.L().Info("hunter_email_verification_debug", + zap.String("message", "performing request"), + zap.String("url", url), + ) + } + + resp, err := http.Get(url) + if err != nil { + if h.debug { + debug.PrintInfo("failed to perform request") + debug.PrintError(err) + } + zap.L().Error("hunter_email_verification", + zap.String("message", "failed to perform request"), + zap.Error(err), + ) + return hunterEmailVerifyData, err + } + defer resp.Body.Close() + + b, err := io.ReadAll(resp.Body) + if err != nil { + if h.debug { + debug.PrintInfo("failed to read response body") + debug.PrintError(err) + } + zap.L().Error("hunter_email_verification", + zap.String("message", "failed to read response body"), + zap.Error(err), + ) + return hunterEmailVerifyData, err + } + + if resp.StatusCode != 200 { + if h.debug { + debug.PrintInfo("received error status code") + debug.PrintJson(fmt.Sprintf("Status Code: %d\n", resp.StatusCode)) + debug.PrintJson(fmt.Sprintf("Body: %s\n", string(b))) + zap.L().Info("hunter_email_verification_debug", + zap.String("message", "received error status code"), + zap.Int("status_code", resp.StatusCode), + zap.String("body_error", string(b)), + ) + } + zap.L().Error("hunter_email_verification", + zap.String("message", "received error status code"), + zap.Int("status_code", resp.StatusCode), + zap.String("body_error", string(b)), + ) + return hunterEmailVerifyData, fmt.Errorf("received error status code: %d", resp.StatusCode) + } + + if h.debug { + debug.PrintInfo("unmarshalled response body") + debug.PrintJson(fmt.Sprintf("Body: %s\n", string(b))) + zap.L().Info("hunter_email_verification_debug", + zap.String("message", "unmarshalled response body"), + zap.String("body", string(b)), + ) + } + + var hunterEmailVerifyResult sqlite.HunterEmailVerifyResponse + err = json.Unmarshal(b, &hunterEmailVerifyResult) + if err != nil { + if h.debug { + debug.PrintInfo("failed to unmarshal response body") + debug.PrintError(err) + } + zap.L().Error("hunter_email_verification", + zap.String("message", "failed to unmarshal response body"), + zap.Error(err), + ) + return hunterEmailVerifyData, err + } + + hunterEmailVerifyData = hunterEmailVerifyResult.Data + + return hunterEmailVerifyData, nil +} + +func (h *HunterIO) CompanyEnrichment(domain string) (sqlite.CompanyData, error) { + var companyData sqlite.CompanyData + + if h.debug { + debug.PrintInfo("performing company enrichment") + zap.L().Info("hunter_company_enrichment_debug", + zap.String("message", "performing company enrichment"), + ) + } + + url := COMPANY_ENRICHMENT + url = strings.Replace(url, "{{domain}}", domain, -1) + url = strings.Replace(url, "{{apikey}}", h.apiKey, -1) + + if h.debug { + debug.PrintInfo("performing request") + debug.PrintInfo(fmt.Sprintf("URL: %s\n", url)) + zap.L().Info("hunter_company_enrichment_debug", + zap.String("message", "performing request"), + zap.String("url", url), + ) + } + + resp, err := http.Get(url) + if err != nil { + if h.debug { + debug.PrintInfo("failed to perform request") + debug.PrintError(err) + } + zap.L().Error("hunter_company_enrichment", + zap.String("message", "failed to perform request"), + zap.Error(err), + ) + return companyData, err + } + defer resp.Body.Close() + + b, err := io.ReadAll(resp.Body) + if err != nil { + if h.debug { + debug.PrintInfo("failed to read response body") + debug.PrintError(err) + } + zap.L().Error("hunter_company_enrichment", + zap.String("message", "failed to read response body"), + zap.Error(err), + ) + return companyData, err + } + + if resp.StatusCode != 200 { + if h.debug { + debug.PrintInfo("received error status code") + debug.PrintJson(fmt.Sprintf("Status Code: %d\n", resp.StatusCode)) + debug.PrintJson(fmt.Sprintf("Body: %s\n", string(b))) + zap.L().Info("hunter_company_enrichment_debug", + zap.String("message", "received error status code"), + zap.Int("status_code", resp.StatusCode), + zap.String("body_error", string(b)), + ) + } + zap.L().Error("hunter_company_enrichment", + zap.String("message", "received error status code"), + zap.Int("status_code", resp.StatusCode), + zap.String("body_error", string(b)), + ) + return companyData, fmt.Errorf("received error status code: %d", resp.StatusCode) + } + + if h.debug { + debug.PrintInfo("unmarshalled response body") + debug.PrintJson(fmt.Sprintf("Body: %s\n", string(b))) + zap.L().Info("hunter_company_enrichment_debug", + zap.String("message", "unmarshalled response body"), + zap.String("body", string(b)), + ) + } + + var hunterCompanyEnrichmentResult sqlite.HunterCompanyEnrichmentResponse + err = json.Unmarshal(b, &hunterCompanyEnrichmentResult) + if err != nil { + if h.debug { + debug.PrintInfo("failed to unmarshal response body") + debug.PrintError(err) + } + zap.L().Error("hunter_company_enrichment", + zap.String("message", "failed to unmarshal response body"), + zap.Error(err), + ) + return companyData, err + } + + companyData = hunterCompanyEnrichmentResult.Data + + return companyData, nil +} + +func (h *HunterIO) PersonEnrichment(email string) (sqlite.PersonData, error) { + var personData sqlite.PersonData + + if h.debug { + debug.PrintInfo("performing person enrichment") + zap.L().Info("hunter_person_enrichment_debug", + zap.String("message", "performing person enrichment"), + ) + } + + url := PERSON_ENRICHMENT + url = strings.Replace(url, "{{email}}", email, -1) + url = strings.Replace(url, "{{apikey}}", h.apiKey, -1) + + if h.debug { + debug.PrintInfo("performing request") + debug.PrintInfo(fmt.Sprintf("URL: %s\n", url)) + zap.L().Info("hunter_person_enrichment_debug", + zap.String("message", "performing request"), + zap.String("url", url), + ) + } + + resp, err := http.Get(url) + if err != nil { + if h.debug { + debug.PrintInfo("failed to perform request") + debug.PrintError(err) + } + zap.L().Error("hunter_person_enrichment", + zap.String("message", "failed to perform request"), + zap.Error(err), + ) + return personData, err + } + defer resp.Body.Close() + + b, err := io.ReadAll(resp.Body) + if err != nil { + if h.debug { + debug.PrintInfo("failed to read response body") + debug.PrintError(err) + } + zap.L().Error("hunter_person_enrichment", + zap.String("message", "failed to read response body"), + zap.Error(err), + ) + return personData, err + } + + if resp.StatusCode != 200 { + if h.debug { + debug.PrintInfo("received error status code") + debug.PrintJson(fmt.Sprintf("Status Code: %d\n", resp.StatusCode)) + debug.PrintJson(fmt.Sprintf("Body: %s\n", string(b))) + zap.L().Info("hunter_person_enrichment_debug", + zap.String("message", "received error status code"), + zap.Int("status_code", resp.StatusCode), + zap.String("body_error", string(b)), + ) + } + zap.L().Error("hunter_person_enrichment", + zap.String("message", "received error status code"), + zap.Int("status_code", resp.StatusCode), + zap.String("body_error", string(b)), + ) + return personData, fmt.Errorf("received error status code: %d", resp.StatusCode) + } + + if h.debug { + debug.PrintInfo("unmarshalled response body") + debug.PrintJson(fmt.Sprintf("Body: %s\n", string(b))) + zap.L().Info("hunter_person_enrichment_debug", + zap.String("message", "unmarshalled response body"), + zap.String("body", string(b)), + ) + } + + var hunterPersonEnrichmentResult sqlite.HunterPersonEnrichmentResponse + err = json.Unmarshal(b, &hunterPersonEnrichmentResult) + if err != nil { + if h.debug { + debug.PrintInfo("failed to unmarshal response body") + debug.PrintError(err) + } + zap.L().Error("hunter_person_enrichment", + zap.String("message", "failed to unmarshal response body"), + zap.Error(err), + ) + return personData, err + } + + personData = hunterPersonEnrichmentResult.Data + + return personData, nil +} + +func (h *HunterIO) CombinedEnrichment(email string) (sqlite.CombinedData, error) { + var combinedData sqlite.CombinedData + + if h.debug { + debug.PrintInfo("performing combined enrichment") + zap.L().Info("hunter_combined_enrichment_debug", + zap.String("message", "performing combined enrichment"), + ) + } + + url := COMBINED_ENRICHMENT + url = strings.Replace(url, "{{email}}", email, -1) + url = strings.Replace(url, "{{apikey}}", h.apiKey, -1) + + if h.debug { + debug.PrintInfo("performing request") + debug.PrintInfo(fmt.Sprintf("URL: %s\n", url)) + zap.L().Info("hunter_combined_enrichment_debug", + zap.String("message", "performing request"), + zap.String("url", url), + ) + } + + resp, err := http.Get(url) + if err != nil { + if h.debug { + debug.PrintInfo("failed to perform request") + debug.PrintError(err) + } + zap.L().Error("hunter_combined_enrichment", + zap.String("message", "failed to perform request"), + zap.Error(err), + ) + return combinedData, err + } + defer resp.Body.Close() + + b, err := io.ReadAll(resp.Body) + if err != nil { + if h.debug { + debug.PrintInfo("failed to read response body") + debug.PrintError(err) + } + zap.L().Error("hunter_combined_enrichment", + zap.String("message", "failed to read response body"), + zap.Error(err), + ) + return combinedData, err + } + + if resp.StatusCode != 200 { + if h.debug { + debug.PrintInfo("received error status code") + debug.PrintJson(fmt.Sprintf("Status Code: %d\n", resp.StatusCode)) + debug.PrintJson(fmt.Sprintf("Body: %s\n", string(b))) + zap.L().Info("hunter_combined_enrichment_debug", + zap.String("message", "received error status code"), + zap.Int("status_code", resp.StatusCode), + zap.String("body_error", string(b)), + ) + } + zap.L().Error("hunter_combined_enrichment", + zap.String("message", "received error status code"), + zap.Int("status_code", resp.StatusCode), + zap.String("body_error", string(b)), + ) + return combinedData, fmt.Errorf("received error status code: %d", resp.StatusCode) + } + + if h.debug { + debug.PrintInfo("unmarshalled response body") + debug.PrintJson(fmt.Sprintf("Body: %s\n", string(b))) + zap.L().Info("hunter_combined_enrichment_debug", + zap.String("message", "unmarshalled response body"), + zap.String("body", string(b)), + ) + } + + var hunterCombinedEnrichmentResult sqlite.HunterCombinedEnrichmentResponse + err = json.Unmarshal(b, &hunterCombinedEnrichmentResult) + if err != nil { + if h.debug { + debug.PrintInfo("failed to unmarshal response body") + debug.PrintError(err) + } + zap.L().Error("hunter_combined_enrichment", + zap.String("message", "failed to unmarshal response body"), + zap.Error(err), + ) + return combinedData, err + } + + combinedData = hunterCombinedEnrichmentResult.Data + + return combinedData, nil +} diff --git a/internal/pretty/trees.go b/internal/pretty/trees.go index 45fa2ba..cd79b7c 100644 --- a/internal/pretty/trees.go +++ b/internal/pretty/trees.go @@ -70,7 +70,7 @@ func WhoIsTree(root string, record sqlite.WhoisRecord) { technicalContactTree.Child("Telephone: " + record.TechnicalContact.Telephone) // Root Tree Children - rootTree.Child("Contact Email: " + record.ContactEmail) + rootTree.Child("Contact HunterEmail: " + record.ContactEmail) rootTree.Child("Created Date: " + record.CreatedDate) rootTree.Child("Created Date Normalized: " + record.CreatedDateNormalized) rootTree.Child("Domain Name: " + record.DomainName) @@ -102,3 +102,244 @@ func WhoIsTree(root string, record sqlite.WhoisRecord) { // Print Tree fmt.Println(rootTree) } + +func HunterDomainTree(root string, record sqlite.HunterDomainData) { + enumeratorStyle := lipgloss.NewStyle().Foreground(purple).MarginRight(1) + rootStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("63")) + itemStyle := lipgloss.NewStyle().Foreground(gray) + + rootTree := tree.Root(root) + + // Root Tree Children + rootTree.Child("Domain: " + record.Domain) + rootTree.Child("Disposable: " + fmt.Sprintf("%t", record.Disposable)) + rootTree.Child("Webmail: " + fmt.Sprintf("%t", record.Webmail)) + rootTree.Child("Accept All: " + fmt.Sprintf("%t", record.AcceptAll)) + rootTree.Child("Pattern: " + record.Pattern) + rootTree.Child("Organization: " + record.Organization) + rootTree.Child("Description: " + record.Description) + rootTree.Child("Industry: " + record.Industry) + rootTree.Child("Twitter: " + record.Twitter) + rootTree.Child("Facebook: " + record.Facebook) + rootTree.Child("Linkedin: " + record.Linkedin) + rootTree.Child("Instagram: " + record.Instagram) + rootTree.Child("Youtube: " + record.Youtube) + + techTree := tree.Root("Technologies") + for _, tech := range record.Technologies { + techTree.Child(tech) + } + rootTree.Child(techTree) + + rootTree.Child("Country: " + record.Country) + rootTree.Child("State: " + record.State) + rootTree.Child("City: " + record.City) + rootTree.Child("Postal Code: " + record.PostalCode) + rootTree.Child("Street: " + record.Street) + rootTree.Child("Headcount: " + record.Headcount) + rootTree.Child("Company Type: " + record.CompanyType) + + emailTree := tree.Root("Emails") + for _, email := range record.Emails { + emailTree.Child(email.ToTree()) + } + rootTree.Child(emailTree) + + linkedDomainTree := tree.Root("Linked Domains") + for _, domain := range record.LinkedDomains { + linkedDomainTree.Child(domain) + } + rootTree.Child(linkedDomainTree) + + // Styles + rootTree.Enumerator(tree.RoundedEnumerator) + rootTree.EnumeratorStyle(enumeratorStyle) + rootTree.RootStyle(rootStyle) + rootTree.ItemStyle(itemStyle) + + // Print Tree + fmt.Println(rootTree) +} + +func HunterCompanyEnrichmentTree(root string, record sqlite.CompanyData) { + enumeratorStyle := lipgloss.NewStyle().Foreground(purple).MarginRight(1) + rootStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("63")) + itemStyle := lipgloss.NewStyle().Foreground(gray) + + rootTree := tree.Root(root) + + // Root Tree Children + rootTree.Child("ID: " + record.ID) + rootTree.Child("Name: " + record.Name) + rootTree.Child("Legal Name: " + record.LegalName) + rootTree.Child("Domain: " + record.Domain) + rootTree.Child(record.DomainAliasesTree()) + rootTree.Child(record.SiteTree()) + rootTree.Child(record.CategoryTree()) + rootTree.Child(record.TagsTree()) + rootTree.Child("Description: " + record.Description) + rootTree.Child("Founded Year: " + fmt.Sprintf("%d", record.FoundedYear)) + rootTree.Child("Location: " + record.Location) + rootTree.Child("Time Zone: " + record.TimeZone) + rootTree.Child("UTC Offset: " + fmt.Sprintf("%d", record.UTCOffset)) + rootTree.Child(record.GeoTree()) + rootTree.Child("Logo: " + record.Logo) + rootTree.Child(record.FacebookTree()) + rootTree.Child(record.LinkedInTree()) + rootTree.Child(record.TwitterTree()) + rootTree.Child(record.CrunchbaseTree()) + rootTree.Child(record.YouTubeTree()) + rootTree.Child("Email Provider: " + record.EmailProvider) + rootTree.Child("Type: " + record.Type) + rootTree.Child("Ticker: " + record.Ticker) + rootTree.Child(record.IdentifiersTree()) + rootTree.Child("Phone: " + record.Phone) + rootTree.Child(record.MetricsTree()) + rootTree.Child("Indexed At: " + record.IndexedAt) + rootTree.Child(record.TechTree()) + rootTree.Child(record.TechCategoriesTree()) + rootTree.Child(record.ParentTree()) + rootTree.Child(record.UltimateParentTree()) + + // Styles + rootTree.Enumerator(tree.RoundedEnumerator) + rootTree.EnumeratorStyle(enumeratorStyle) + rootTree.RootStyle(rootStyle) + rootTree.ItemStyle(itemStyle) + + // Print Tree + fmt.Println(rootTree) +} + +func HunterPersonEnrichmentTree(root string, record sqlite.PersonData) { + enumeratorStyle := lipgloss.NewStyle().Foreground(purple).MarginRight(1) + rootStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("63")) + itemStyle := lipgloss.NewStyle().Foreground(gray) + + rootTree := tree.Root(root) + + // Root Tree Children + rootTree.Child("ID: " + record.ID) + rootTree.Child(record.NameTree()) + rootTree.Child("Email: " + record.Email) + rootTree.Child("Location: " + record.Location) + rootTree.Child("Time Zone: " + record.TimeZone) + rootTree.Child("UTC Offset: " + fmt.Sprintf("%d", record.UTCOffset)) + rootTree.Child(record.GeoTree()) + rootTree.Child("Bio: " + record.Bio) + rootTree.Child("Site: " + record.Site) + rootTree.Child("Avatar: " + record.Avatar) + rootTree.Child(record.EmploymentTree()) + rootTree.Child(record.FacebookTree()) + rootTree.Child(record.GitHubTree()) + rootTree.Child(record.TwitterTree()) + rootTree.Child(record.LinkedInTree()) + rootTree.Child(record.GooglePlusTree()) + rootTree.Child(record.GravatarTree()) + rootTree.Child("Fuzzy: " + fmt.Sprintf("%t", record.Fuzzy)) + rootTree.Child("Email Provider: " + record.EmailProvider) + rootTree.Child("Indexed At: " + record.IndexedAt) + rootTree.Child("Phone: " + record.Phone) + rootTree.Child("Active At: " + record.ActiveAt) + rootTree.Child("Inactive At: " + record.InactiveAt) + + // Styles + rootTree.Enumerator(tree.RoundedEnumerator) + rootTree.EnumeratorStyle(enumeratorStyle) + rootTree.RootStyle(rootStyle) + rootTree.ItemStyle(itemStyle) + + // Print Tree + fmt.Println(rootTree) +} + +func HunterCombinedEnrichmentTree(root string, record sqlite.CombinedData) { + enumeratorStyle := lipgloss.NewStyle().Foreground(purple).MarginRight(1) + rootStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("63")) + itemStyle := lipgloss.NewStyle().Foreground(gray) + + rootTree := tree.Root(root) + + // Root Tree Children + rootTree.Child(personTree(record.Person)) + rootTree.Child(companyTree(record.Company)) + + // Styles + rootTree.Enumerator(tree.RoundedEnumerator) + rootTree.EnumeratorStyle(enumeratorStyle) + rootTree.RootStyle(rootStyle) + rootTree.ItemStyle(itemStyle) + + // Print Tree + fmt.Println(rootTree) +} + +func companyTree(record sqlite.CompanyData) *tree.Tree { + companyTree := tree.Root("Company") + + // Company Tree Children + companyTree.Child("ID: " + record.ID) + companyTree.Child("Name: " + record.Name) + companyTree.Child("Legal Name: " + record.LegalName) + companyTree.Child("Domain: " + record.Domain) + companyTree.Child(record.DomainAliasesTree()) + companyTree.Child(record.SiteTree()) + companyTree.Child(record.CategoryTree()) + companyTree.Child(record.TagsTree()) + companyTree.Child("Description: " + record.Description) + companyTree.Child("Founded Year: " + fmt.Sprintf("%d", record.FoundedYear)) + companyTree.Child("Location: " + record.Location) + companyTree.Child("Time Zone: " + record.TimeZone) + companyTree.Child("UTC Offset: " + fmt.Sprintf("%d", record.UTCOffset)) + companyTree.Child(record.GeoTree()) + companyTree.Child("Logo: " + record.Logo) + companyTree.Child(record.FacebookTree()) + companyTree.Child(record.LinkedInTree()) + companyTree.Child(record.TwitterTree()) + companyTree.Child(record.CrunchbaseTree()) + companyTree.Child(record.YouTubeTree()) + companyTree.Child("Email Provider: " + record.EmailProvider) + companyTree.Child("Type: " + record.Type) + companyTree.Child("Ticker: " + record.Ticker) + companyTree.Child(record.IdentifiersTree()) + companyTree.Child("Phone: " + record.Phone) + companyTree.Child(record.MetricsTree()) + companyTree.Child("Indexed At: " + record.IndexedAt) + companyTree.Child(record.TechTree()) + companyTree.Child(record.TechCategoriesTree()) + companyTree.Child(record.ParentTree()) + companyTree.Child(record.UltimateParentTree()) + + return companyTree +} + +func personTree(record sqlite.PersonData) *tree.Tree { + personTree := tree.Root("Person") + + // Person Tree Children + personTree.Child("ID: " + record.ID) + personTree.Child(record.NameTree()) + personTree.Child("Email: " + record.Email) + personTree.Child("Location: " + record.Location) + personTree.Child("Time Zone: " + record.TimeZone) + personTree.Child("UTC Offset: " + fmt.Sprintf("%d", record.UTCOffset)) + personTree.Child(record.GeoTree()) + personTree.Child("Bio: " + record.Bio) + personTree.Child("Site: " + record.Site) + personTree.Child("Avatar: " + record.Avatar) + personTree.Child(record.EmploymentTree()) + personTree.Child(record.FacebookTree()) + personTree.Child(record.GitHubTree()) + personTree.Child(record.TwitterTree()) + personTree.Child(record.LinkedInTree()) + personTree.Child(record.GooglePlusTree()) + personTree.Child(record.GravatarTree()) + personTree.Child("Fuzzy: " + fmt.Sprintf("%t", record.Fuzzy)) + personTree.Child("Email Provider: " + record.EmailProvider) + personTree.Child("Indexed At: " + record.IndexedAt) + personTree.Child("Phone: " + record.Phone) + personTree.Child("Active At: " + record.ActiveAt) + personTree.Child("Inactive At: " + record.InactiveAt) + + return personTree +} diff --git a/internal/sqlite/gorm.go b/internal/sqlite/gorm.go index affb4da..bdc152d 100644 --- a/internal/sqlite/gorm.go +++ b/internal/sqlite/gorm.go @@ -51,7 +51,8 @@ func InitDB(dbPath string) (*gorm.DB, error) { } // Auto migrate your models - err = db.AutoMigrate(&Result{}, &Creds{}, &QueryOptions{}, &Creds{}, &WhoisRecord{}, &SubdomainRecord{}, &HistoryRecord{}, &LookupResult{}) + err = db.AutoMigrate(&Result{}, &Creds{}, &QueryOptions{}, &Creds{}, &WhoisRecord{}, &SubdomainRecord{}, + &HistoryRecord{}, &LookupResult{}, &HunterDomainData{}, &HunterEmail{}, &PersonData{}) if err != nil { zap.L().Error("Failed to migrate database", zap.Error(err)) return nil, fmt.Errorf("failed to migrate database: %w", err) @@ -255,3 +256,64 @@ func StoreIPLookup(ipLookup []LookupResult) error { return lastErr } + +func StoreHunterDomain(hunterDomain HunterDomainData) error { + db := GetDB() + + // Use OnConflict clause to handle duplicates + err := db.Clauses(clause.OnConflict{DoNothing: true}).Create(&hunterDomain).Error + if err != nil { + zap.L().Error("store_hunter_domain", + zap.String("message", "failed to store hunter domain"), + zap.Error(err)) + return err + } + + return nil +} + +func StoreHunterEmails(hunterEmails []HunterEmail) error { + if len(hunterEmails) == 0 { + return nil + } + + zap.L().Info("Storing hunter emails", zap.Int("count", len(hunterEmails))) + db := GetDB() + + // Use batch insert with conflict handling + const batchSize = 100 + var lastErr error + + for i := 0; i < len(hunterEmails); i += batchSize { + end := i + batchSize + if end > len(hunterEmails) { + end = len(hunterEmails) + } + + batch := hunterEmails[i:end] + // Use Clauses with OnConflict DoNothing to skip conflicts + err := db.Clauses(clause.OnConflict{DoNothing: true}).CreateInBatches(&batch, batchSize).Error + if err != nil { + zap.L().Warn("Error storing some hunter emails", zap.Error(err)) + lastErr = err + // Continue with next batch despite error + } + } + + return lastErr +} + +func StorePersonData(personData PersonData) error { + db := GetDB() + + // Use OnConflict clause to handle duplicates + err := db.Clauses(clause.OnConflict{DoNothing: true}).Create(&personData).Error + if err != nil { + zap.L().Error("store_person_data", + zap.String("message", "failed to store person data"), + zap.Error(err)) + return err + } + + return nil +} diff --git a/internal/sqlite/hunter.io.go b/internal/sqlite/hunter.io.go new file mode 100644 index 0000000..f78a0ed --- /dev/null +++ b/internal/sqlite/hunter.io.go @@ -0,0 +1,751 @@ +package sqlite + +import ( + "fmt" + "github.com/charmbracelet/lipgloss/tree" + "gorm.io/gorm" +) + +// HunterDomainSearchResult represents the response from Hunter.io domain search API +type HunterDomainSearchResult struct { + Data HunterDomainData `json:"data" gorm:"embedded;embeddedPrefix:data_"` + Meta HunterMeta `json:"meta" gorm:"embedded;embeddedPrefix:meta_"` +} + +// HunterDomainData contains the main domain information +type HunterDomainData struct { + gorm.Model + Domain string `json:"domain" gorm:"unique"` + Disposable bool `json:"disposable"` + Webmail bool `json:"webmail"` + AcceptAll bool `json:"accept_all"` + Pattern string `json:"pattern"` + Organization string `json:"organization"` + Description string `json:"description"` + Industry string `json:"industry"` + Twitter string `json:"twitter"` + Facebook string `json:"facebook"` + Linkedin string `json:"linkedin"` + Instagram string `json:"instagram"` + Youtube string `json:"youtube"` + Technologies []string `json:"technologies" gorm:"serializer:json"` + Country string `json:"country"` + State string `json:"state"` + City string `json:"city"` + PostalCode string `json:"postal_code"` + Street string `json:"street"` + Headcount string `json:"headcount"` + CompanyType string `json:"company_type"` + Emails []HunterEmail `json:"emails" gorm:"serializer:json"` + LinkedDomains []string `json:"linked_domains" gorm:"serializer:json"` +} + +func (h *HunterDomainData) String() string { + return fmt.Sprintf("Domain: %s\nDisposable: %t\nWebmail: %t\nAcceptAll: %t\nPattern: %s\nOrganization: %s\nDescription: %s\nIndustry: %s\nTwitter: %s\nFacebook: %s\nLinkedin: %s\nInstagram: %s\nYoutube: %s\nTechnologies: %v\nCountry: %s\nState: %s\nCity: %s\nPostalCode: %s\nStreet: %s\nHeadcount: %s\nCompanyType: %s\nEmails: %v\nLinkedDomains: %v\n", + h.Domain, h.Disposable, h.Webmail, h.AcceptAll, h.Pattern, h.Organization, h.Description, h.Industry, h.Twitter, h.Facebook, h.Linkedin, h.Instagram, h.Youtube, h.Technologies, h.Country, h.State, h.City, h.PostalCode, h.Street, h.Headcount, h.CompanyType, h.Emails, h.LinkedDomains) +} + +func (HunterDomainData) TableName() string { + return "hunter_domain" +} + +// HunterEmail represents an email found for the domain +type HunterEmail struct { + gorm.Model + Domain string `json:"domain,omitempty"` + Value string `json:"value" gorm:"unique"` + Type string `json:"type"` + Confidence int `json:"confidence"` + Sources []HunterSource `json:"sources" gorm:"serializer:json"` + FirstName string `json:"first_name"` + LastName string `json:"last_name"` + Position string `json:"position"` + PositionRaw string `json:"position_raw"` + Seniority string `json:"seniority"` + Department string `json:"department"` + Linkedin string `json:"linkedin"` + Twitter string `json:"twitter"` + PhoneNumber string `json:"phone_number"` + Verification HunterVerification `json:"verification" gorm:"embedded;embeddedPrefix:verification_"` +} + +func (he *HunterEmail) ToTree() *tree.Tree { + emailTree := tree.Root(he.Value) + emailTree.Child("Type: " + he.Type) + emailTree.Child("Confidence: " + fmt.Sprintf("%d", he.Confidence)) + emailTree.Child("FirstName: " + he.FirstName) + emailTree.Child("LastName: " + he.LastName) + emailTree.Child("Position: " + he.Position) + emailTree.Child("PositionRaw: " + he.PositionRaw) + emailTree.Child("Seniority: " + he.Seniority) + emailTree.Child("Department: " + he.Department) + emailTree.Child("Linkedin: " + he.Linkedin) + emailTree.Child("Twitter: " + he.Twitter) + emailTree.Child("PhoneNumber: " + he.PhoneNumber) + emailTree.Child(he.Verification.ToTree()) + return emailTree +} + +func (he *HunterEmail) String() string { + return fmt.Sprintf("Value: %s\nType: %s\nConfidence: %d\nSources: %v\nFirstName: %s\nLastName: %s\nPosition: %s\nPositionRaw: %s\nSeniority: %s\nDepartment: %s\nLinkedin: %s\nTwitter: %s\nPhoneNumber: %s\nVerification: %v\n", + he.Value, he.Type, he.Confidence, he.Sources, he.FirstName, he.LastName, he.Position, he.PositionRaw, he.Seniority, he.Department, he.Linkedin, he.Twitter, he.PhoneNumber, he.Verification) +} + +func (HunterEmail) TableName() string { + return "hunter_email" +} + +// HunterSource represents where an email was found +type HunterSource struct { + Domain string `json:"domain"` + URI string `json:"uri"` + ExtractedOn string `json:"extracted_on"` + LastSeenOn string `json:"last_seen_on"` + StillOnPage bool `json:"still_on_page"` +} + +// HunterVerification represents the verification status of an email +type HunterVerification struct { + Date string `json:"date"` + Status string `json:"status"` +} + +func (hv *HunterVerification) ToTree() *tree.Tree { + verificationTree := tree.Root("Verification") + verificationTree.Child("Date: " + hv.Date) + verificationTree.Child("Status: " + hv.Status) + return verificationTree +} + +// HunterMeta contains metadata about the API response +type HunterMeta struct { + Results int `json:"results"` + Limit int `json:"limit"` + Offset int `json:"offset"` + Params HunterSearchParams `json:"params" gorm:"embedded;embeddedPrefix:params_"` +} + +// HunterSearchParams contains the parameters used in the search +type HunterSearchParams struct { + Domain string `json:"domain"` + Company string `json:"company"` + Type string `json:"type"` + Seniority string `json:"seniority"` + Department string `json:"department"` +} + +// HunterEmailFinderResponse represents the response from Hunter.io email finder API +type HunterEmailFinderResponse struct { + Data HunterEmailFinderData `json:"data" gorm:"embedded;embeddedPrefix:data_"` + Meta EmailFinderMeta `json:"meta" gorm:"embedded;embeddedPrefix:meta_"` +} + +// HunterEmailFinderData contains the main email information +type HunterEmailFinderData struct { + FirstName string `json:"first_name"` + LastName string `json:"last_name"` + Email string `json:"email"` + Score int `json:"score"` + Domain string `json:"domain"` + AcceptAll bool `json:"accept_all"` + Position string `json:"position"` + Twitter string `json:"twitter"` + LinkedinURL string `json:"linkedin_url"` + PhoneNumber string `json:"phone_number"` + Company string `json:"company"` + Sources []HunterSource `json:"sources" gorm:"serializer:json"` + Verification HunterVerification `json:"verification" gorm:"embedded;embeddedPrefix:verification_"` +} + +func (he *HunterEmailFinderData) String() string { + return fmt.Sprintf("FirstName: %s\nLastName: %s\nEmail: %s\nScore: %d\nDomain: %s\nAcceptAll: %t\nPosition: %s\nTwitter: %s\nLinkedinURL: %s\nPhoneNumber: %s\nCompany: %s\nSources: %v\nVerification: %v\n", + he.FirstName, he.LastName, he.Email, he.Score, he.Domain, he.AcceptAll, he.Position, he.Twitter, he.LinkedinURL, he.PhoneNumber, he.Company, he.Sources, he.Verification) +} + +// EmailFinderMeta contains metadata about the API response +type EmailFinderMeta struct { + Params EmailFinderParams `json:"params" gorm:"embedded;embeddedPrefix:params_"` +} + +// EmailFinderParams contains the parameters used in the search +type EmailFinderParams struct { + FirstName string `json:"first_name"` + LastName string `json:"last_name"` + FullName string `json:"full_name"` + Domain string `json:"domain"` + Company string `json:"company"` + MaxDuration string `json:"max_duration"` +} + +func (HunterEmailFinderResponse) TableName() string { + return "hunter_email_finder" +} + +// HunterEmailVerifyResponse represents the response from Hunter.io email verification API +type HunterEmailVerifyResponse struct { + Data HunterEmailVerifyData `json:"data" gorm:"embedded;embeddedPrefix:data_"` + Meta EmailVerifyMeta `json:"meta" gorm:"embedded;embeddedPrefix:meta_"` +} + +// HunterEmailVerifyData contains the email verification information +type HunterEmailVerifyData struct { + Status string `json:"status"` + Result string `json:"result"` + DeprecationNotice string `json:"_deprecation_notice"` + Score int `json:"score"` + Email string `json:"email"` + Regexp bool `json:"regexp"` + Gibberish bool `json:"gibberish"` + Disposable bool `json:"disposable"` + Webmail bool `json:"webmail"` + MXRecords bool `json:"mx_records"` + SMTPServer bool `json:"smtp_server"` + SMTPCheck bool `json:"smtp_check"` + AcceptAll bool `json:"accept_all"` + Block bool `json:"block"` + Sources []HunterSource `json:"sources" gorm:"serializer:json"` +} + +func (ev *HunterEmailVerifyData) String() string { + return fmt.Sprintf("Status: %s\nResult: %s\nDeprecationNotice: %s\nScore: %d\nEmail: %s\nRegexp: %t\nGibberish: %t\nDisposable: %t\nWebmail: %t\nMXRecords: %t\nSMTPServer: %t\nSMTPCheck: %t\nAcceptAll: %t\nBlock: %t\nSources: %v\n", + ev.Status, ev.Result, ev.DeprecationNotice, ev.Score, ev.Email, ev.Regexp, ev.Gibberish, ev.Disposable, ev.Webmail, ev.MXRecords, ev.SMTPServer, ev.SMTPCheck, ev.AcceptAll, ev.Block, ev.Sources) +} + +// EmailVerifyMeta contains metadata about the API response +type EmailVerifyMeta struct { + Params EmailVerifyParams `json:"params" gorm:"embedded;embeddedPrefix:params_"` +} + +// EmailVerifyParams contains the parameters used in the verification +type EmailVerifyParams struct { + Email string `json:"email"` +} + +// HunterCompanyEnrichmentResponse represents the response from Hunter.io company enrichment API +type HunterCompanyEnrichmentResponse struct { + Data CompanyData `json:"data" gorm:"embedded;embeddedPrefix:data_"` + Meta CompanyMeta `json:"meta" gorm:"embedded;embeddedPrefix:meta_"` +} + +// CompanyData contains the detailed company information +type CompanyData struct { + ID string `json:"id"` + Name string `json:"name"` + LegalName string `json:"legalName"` + Domain string `json:"domain"` + DomainAliases []string `json:"domainAliases" gorm:"serializer:json"` + Site CompanySite `json:"site" gorm:"embedded;embeddedPrefix:site_"` + Category Category `json:"category" gorm:"embedded;embeddedPrefix:category_"` + Tags []string `json:"tags" gorm:"serializer:json"` + Description string `json:"description"` + FoundedYear int `json:"foundedYear"` + Location string `json:"location"` + TimeZone string `json:"timeZone"` + UTCOffset int `json:"utcOffset"` + Geo Geography `json:"geo" gorm:"embedded;embeddedPrefix:geo_"` + Logo string `json:"logo"` + Facebook Facebook `json:"facebook" gorm:"embedded;embeddedPrefix:facebook_"` + LinkedIn LinkedIn `json:"linkedin" gorm:"embedded;embeddedPrefix:linkedin_"` + Twitter Twitter `json:"twitter" gorm:"embedded;embeddedPrefix:twitter_"` + Crunchbase Crunchbase `json:"crunchbase" gorm:"embedded;embeddedPrefix:crunchbase_"` + YouTube YouTube `json:"youtube" gorm:"embedded;embeddedPrefix:youtube_"` + EmailProvider string `json:"emailProvider"` + Type string `json:"type"` + Ticker string `json:"ticker"` + Identifiers Identifiers `json:"identifiers" gorm:"embedded;embeddedPrefix:identifiers_"` + Phone string `json:"phone"` + Metrics Metrics `json:"metrics" gorm:"embedded;embeddedPrefix:metrics_"` + IndexedAt string `json:"indexedAt"` + Tech []string `json:"tech" gorm:"serializer:json"` + TechCategories []string `json:"techCategories" gorm:"serializer:json"` + Parent ParentCompany `json:"parent" gorm:"embedded;embeddedPrefix:parent_"` + UltimateParent ParentCompany `json:"ultimateParent" gorm:"embedded;embeddedPrefix:ultimate_parent_"` +} + +func (cd *CompanyData) String() string { + return fmt.Sprintf("ID: %s\nName: %s\nLegalName: %s\nDomain: %s\nDomainAliases: %v\nSite: %v\nCategory: %v\nTags: %v\nDescription: %s\nFoundedYear: %d\nLocation: %s\nTimeZone: %s\nUTCOffset: %d\nGeo: %v\nLogo: %s\nFacebook: %v\nLinkedIn: %v\nTwitter: %v\nCrunchbase: %v\nYouTube: %v\nEmailProvider: %s\nType: %s\nTicker: %s\nIdentifiers: %v\nPhone: %s\nMetrics: %v\nIndexedAt: %s\nTech: %v\nTechCategories: %v\nParent: %v\nUltimateParent: %v\n", + cd.ID, cd.Name, cd.LegalName, cd.Domain, cd.DomainAliases, cd.Site, cd.Category, cd.Tags, cd.Description, cd.FoundedYear, cd.Location, cd.TimeZone, cd.UTCOffset, cd.Geo, cd.Logo, cd.Facebook, cd.LinkedIn, cd.Twitter, cd.Crunchbase, cd.YouTube, cd.EmailProvider, cd.Type, cd.Ticker, cd.Identifiers, cd.Phone, cd.Metrics, cd.IndexedAt, cd.Tech, cd.TechCategories, cd.Parent, cd.UltimateParent) +} + +func (cd *CompanyData) DomainAliasesTree() *tree.Tree { + domainAliasesTree := tree.Root("Domain Aliases") + for _, domainAlias := range cd.DomainAliases { + domainAliasesTree.Child(domainAlias) + } + return domainAliasesTree +} + +func (cd *CompanyData) SiteTree() *tree.Tree { + siteTree := tree.Root("Site") + phoneTree := tree.Root("Phone Numbers") + for _, phoneNumber := range cd.Site.PhoneNumbers { + phoneTree.Child(phoneNumber) + } + emailTree := tree.Root("Email Addresses") + for _, emailAddress := range cd.Site.EmailAddresses { + emailTree.Child(emailAddress) + } + siteTree.Child(phoneTree) + siteTree.Child(emailTree) + return siteTree +} + +func (cd *CompanyData) CategoryTree() *tree.Tree { + categoryTree := tree.Root("Category") + categoryTree.Child("Sector: " + cd.Category.Sector) + categoryTree.Child("Industry Group: " + cd.Category.IndustryGroup) + categoryTree.Child("Industry: " + cd.Category.Industry) + categoryTree.Child("Sub Industry: " + cd.Category.SubIndustry) + categoryTree.Child("GICS Code: " + cd.Category.GICSCode) + categoryTree.Child("SIC Code: " + cd.Category.SICCode) + + sic4CodesTree := tree.Root("SIC 4 Codes") + for _, sic4Code := range cd.Category.SIC4Codes { + sic4CodesTree.Child(sic4Code) + } + categoryTree.Child(sic4CodesTree) + + categoryTree.Child("NAICS Code: " + cd.Category.NAICSCode) + + naics6CodesTree := tree.Root("NAICS 6 Codes") + for _, naics6Code := range cd.Category.NAICS6Codes { + naics6CodesTree.Child(naics6Code) + } + categoryTree.Child(naics6CodesTree) + + naics6Codes2022Tree := tree.Root("NAICS 6 Codes 2022") + for _, naics6Code2022 := range cd.Category.NAICS6Codes2022 { + naics6Codes2022Tree.Child(naics6Code2022) + } + categoryTree.Child(naics6Codes2022Tree) + return categoryTree +} + +func (cd *CompanyData) GeoTree() *tree.Tree { + geoTree := tree.Root("Geo") + geoTree.Child("Street Number: " + cd.Geo.StreetNumber) + geoTree.Child("Street Name: " + cd.Geo.StreetName) + geoTree.Child("Sub Premise: " + cd.Geo.SubPremise) + geoTree.Child("Street Address: " + cd.Geo.StreetAddress) + geoTree.Child("City: " + cd.Geo.City) + geoTree.Child("Postal Code: " + cd.Geo.PostalCode) + geoTree.Child("State: " + cd.Geo.State) + geoTree.Child("State Code: " + cd.Geo.StateCode) + geoTree.Child("Country: " + cd.Geo.Country) + geoTree.Child("Country Code: " + cd.Geo.CountryCode) + geoTree.Child("Latitude: " + fmt.Sprintf("%f", cd.Geo.Lat)) + geoTree.Child("Longitude: " + fmt.Sprintf("%f", cd.Geo.Lng)) + return geoTree +} + +func (cd *CompanyData) FacebookTree() *tree.Tree { + facebookTree := tree.Root("Facebook") + facebookTree.Child("Handle: " + cd.Facebook.Handle) + facebookTree.Child("Likes: " + fmt.Sprintf("%d", cd.Facebook.Likes)) + return facebookTree +} + +func (cd *CompanyData) LinkedInTree() *tree.Tree { + linkedinTree := tree.Root("LinkedIn") + linkedinTree.Child("Handle: " + cd.LinkedIn.Handle) + return linkedinTree +} + +func (cd *CompanyData) TwitterTree() *tree.Tree { + twitterTree := tree.Root("Twitter") + twitterTree.Child("Handle: " + cd.Twitter.Handle) + twitterTree.Child("ID: " + cd.Twitter.ID) + twitterTree.Child("Bio: " + cd.Twitter.Bio) + twitterTree.Child("Followers: " + fmt.Sprintf("%d", cd.Twitter.Followers)) + twitterTree.Child("Following: " + fmt.Sprintf("%d", cd.Twitter.Following)) + twitterTree.Child("Location: " + cd.Twitter.Location) + twitterTree.Child("Site: " + cd.Twitter.Site) + twitterTree.Child("Avatar" + cd.Twitter.Avatar) + return twitterTree +} + +func (cd *CompanyData) CrunchbaseTree() *tree.Tree { + crunchbaseTree := tree.Root("Crunchbase") + crunchbaseTree.Child("Handle: " + cd.Crunchbase.Handle) + return crunchbaseTree +} + +func (cd *CompanyData) YouTubeTree() *tree.Tree { + youtubeTree := tree.Root("YouTube") + youtubeTree.Child("Handle: " + cd.YouTube.Handle) + return youtubeTree +} + +func (cd *CompanyData) IdentifiersTree() *tree.Tree { + identifiersTree := tree.Root("Identifiers") + identifiersTree.Child("UsEIN: " + cd.Identifiers.UsEIN) + return identifiersTree +} + +func (cd *CompanyData) MetricsTree() *tree.Tree { + metricsTree := tree.Root("Metrics") + metricsTree.Child("Alexa Us Rank: " + fmt.Sprintf("%d", cd.Metrics.AlexaUsRank)) + metricsTree.Child("Alexa Global Rank: " + fmt.Sprintf("%d", cd.Metrics.AlexaGlobalRank)) + metricsTree.Child("Traffic Rank: " + cd.Metrics.TrafficRank) + metricsTree.Child("Employees: " + cd.Metrics.Employees) + metricsTree.Child("Market Cap: " + cd.Metrics.MarketCap) + metricsTree.Child("Raised: " + cd.Metrics.Raised) + metricsTree.Child("Annual Revenue: " + cd.Metrics.AnnualRevenue) + metricsTree.Child("Estimated Annual Revenue: " + cd.Metrics.EstimatedAnnualRevenue) + metricsTree.Child("Fiscal Year End: " + cd.Metrics.FiscalYearEnd) + return metricsTree +} + +func (cd *CompanyData) TagsTree() *tree.Tree { + tagsTree := tree.Root("Tags") + for _, tag := range cd.Tags { + tagsTree.Child(tag) + } + return tagsTree +} + +func (cd *CompanyData) TechTree() *tree.Tree { + techTree := tree.Root("Tech") + for _, tech := range cd.Tech { + techTree.Child(tech) + } + return techTree +} + +func (cd *CompanyData) TechCategoriesTree() *tree.Tree { + techCategoriesTree := tree.Root("Tech Categories") + for _, techCategory := range cd.TechCategories { + techCategoriesTree.Child(techCategory) + } + return techCategoriesTree +} + +func (cd *CompanyData) ParentTree() *tree.Tree { + parentTree := tree.Root("Parent") + parentTree.Child("Domain: " + cd.Parent.Domain) + return parentTree +} + +func (cd *CompanyData) UltimateParentTree() *tree.Tree { + ultimateParentTree := tree.Root("Ultimate Parent") + ultimateParentTree.Child("Domain: " + cd.UltimateParent.Domain) + return ultimateParentTree +} + +// CompanySite contains contact information from the company website +type CompanySite struct { + PhoneNumbers []string `json:"phoneNumbers" gorm:"serializer:json"` + EmailAddresses []string `json:"emailAddresses" gorm:"serializer:json"` +} + +// Category contains industry classification information +type Category struct { + Sector string `json:"sector"` + IndustryGroup string `json:"industryGroup"` + Industry string `json:"industry"` + SubIndustry string `json:"subIndustry"` + GICSCode string `json:"gicsCode"` + SICCode string `json:"sicCode"` + SIC4Codes []string `json:"sic4Codes" gorm:"serializer:json"` + NAICSCode string `json:"naicsCode"` + NAICS6Codes []string `json:"naics6Codes" gorm:"serializer:json"` + NAICS6Codes2022 []string `json:"naics6Codes2022" gorm:"serializer:json"` +} + +// Geography contains location information +type Geography struct { + StreetNumber string `json:"streetNumber"` + StreetName string `json:"streetName"` + SubPremise string `json:"subPremise"` + StreetAddress string `json:"streetAddress"` + City string `json:"city"` + PostalCode string `json:"postalCode"` + State string `json:"state"` + StateCode string `json:"stateCode"` + Country string `json:"country"` + CountryCode string `json:"countryCode"` + Lat float64 `json:"lat"` + Lng float64 `json:"lng"` +} + +// Identifiers contains company identification numbers +type Identifiers struct { + UsEIN string `json:"usEIN"` +} + +// Metrics contains company performance metrics +type Metrics struct { + AlexaUsRank int `json:"alexaUsRank"` + AlexaGlobalRank int `json:"alexaGlobalRank"` + TrafficRank string `json:"trafficRank"` + Employees string `json:"employees"` + MarketCap string `json:"marketCap"` + Raised string `json:"raised"` + AnnualRevenue string `json:"annualRevenue"` + EstimatedAnnualRevenue string `json:"estimatedAnnualRevenue"` + FiscalYearEnd string `json:"fiscalYearEnd"` +} + +// ParentCompany contains information about parent companies +type ParentCompany struct { + Domain string `json:"domain"` +} + +// CompanyMeta contains metadata about the API response +type CompanyMeta struct { + Domain string `json:"domain"` +} + +func (HunterCompanyEnrichmentResponse) TableName() string { + return "hunter_company_enrichment" +} + +// HunterPersonEnrichmentResponse represents the response from Hunter.io person enrichment API +type HunterPersonEnrichmentResponse struct { + Data PersonData `json:"data" gorm:"embedded;embeddedPrefix:data_"` + Meta PersonMeta `json:"meta" gorm:"embedded;embeddedPrefix:meta_"` +} + +// PersonData contains the detailed person information +type PersonData struct { + gorm.Model + ID string `json:"id"` + Name PersonName `json:"name" gorm:"embedded;embeddedPrefix:name_"` + Email string `json:"email" gorm:"unique"` + Location string `json:"location"` + TimeZone string `json:"timeZone"` + UTCOffset int `json:"utcOffset"` + Geo PersonGeo `json:"geo" gorm:"embedded;embeddedPrefix:geo_"` + Bio string `json:"bio"` + Site string `json:"site"` + Avatar string `json:"avatar"` + Employment Employment `json:"employment" gorm:"embedded;embeddedPrefix:employment_"` + Facebook Facebook `json:"facebook" gorm:"embedded;embeddedPrefix:facebook_"` + GitHub GitHub `json:"github" gorm:"embedded;embeddedPrefix:github_"` + Twitter Twitter `json:"twitter" gorm:"embedded;embeddedPrefix:twitter_"` + LinkedIn LinkedIn `json:"linkedin" gorm:"embedded;embeddedPrefix:linkedin_"` + GooglePlus GooglePlus `json:"googleplus" gorm:"embedded;embeddedPrefix:googleplus_"` + Gravatar Gravatar `json:"gravatar" gorm:"embedded;embeddedPrefix:gravatar_"` + Fuzzy bool `json:"fuzzy"` + EmailProvider string `json:"emailProvider"` + IndexedAt string `json:"indexedAt"` + Phone string `json:"phone"` + ActiveAt string `json:"activeAt"` + InactiveAt string `json:"inactiveAt"` +} + +func (pd *PersonData) String() string { + return fmt.Sprintf("ID: %s\nName: %v\nEmail: %s\nLocation: %s\nTimeZone: %s\nUTCOffset: %d\nGeo: %v\nBio: %s\nSite: %s\nAvatar: %s\nEmployment: %v\nFacebook: %v\nGitHub: %v\nTwitter: %v\nLinkedIn: %v\nGooglePlus: %v\nGravatar: %v\nFuzzy: %t\nEmailProvider: %s\nIndexedAt: %s\nPhone: %s\nActiveAt: %s\nInactiveAt: %s\n", + pd.ID, pd.Name, pd.Email, pd.Location, pd.TimeZone, pd.UTCOffset, pd.Geo, pd.Bio, pd.Site, pd.Avatar, pd.Employment, pd.Facebook, pd.GitHub, pd.Twitter, pd.LinkedIn, pd.GooglePlus, pd.Gravatar, pd.Fuzzy, pd.EmailProvider, pd.IndexedAt, pd.Phone, pd.ActiveAt, pd.InactiveAt) +} + +func (pd *PersonData) NameTree() *tree.Tree { + nameTree := tree.Root("Name") + nameTree.Child("Full Name: " + pd.Name.FullName) + nameTree.Child("Given Name: " + pd.Name.GivenName) + nameTree.Child("Family Name: " + pd.Name.FamilyName) + return nameTree +} + +func (pd *PersonData) GeoTree() *tree.Tree { + geoTree := tree.Root("Geo") + geoTree.Child("City: " + pd.Geo.City) + geoTree.Child("State: " + pd.Geo.State) + geoTree.Child("State Code: " + pd.Geo.StateCode) + geoTree.Child("Country: " + pd.Geo.Country) + geoTree.Child("Country Code: " + pd.Geo.CountryCode) + geoTree.Child("Latitude: " + fmt.Sprintf("%f", pd.Geo.Lat)) + geoTree.Child("Longitude: " + fmt.Sprintf("%f", pd.Geo.Lng)) + return geoTree +} + +func (pd *PersonData) EmploymentTree() *tree.Tree { + employmentTree := tree.Root("Employment") + employmentTree.Child("Domain: " + pd.Employment.Domain) + employmentTree.Child("Name: " + pd.Employment.Name) + employmentTree.Child("Title: " + pd.Employment.Title) + employmentTree.Child("Role: " + pd.Employment.Role) + employmentTree.Child("Sub Role: " + pd.Employment.SubRole) + employmentTree.Child("Seniority: " + pd.Employment.Seniority) + return employmentTree +} + +func (pd *PersonData) FacebookTree() *tree.Tree { + facebookTree := tree.Root("Facebook") + facebookTree.Child("Handle: " + pd.Facebook.Handle) + facebookTree.Child("Likes: " + fmt.Sprintf("%d", pd.Facebook.Likes)) + return facebookTree +} + +func (pd *PersonData) GitHubTree() *tree.Tree { + githubTree := tree.Root("GitHub") + githubTree.Child("Handle: " + pd.GitHub.Handle) + githubTree.Child("ID: " + pd.GitHub.ID) + githubTree.Child("Avatar: " + pd.GitHub.Avatar) + githubTree.Child("Company: " + pd.GitHub.Company) + githubTree.Child("Blog: " + pd.GitHub.Blog) + githubTree.Child("Followers: " + fmt.Sprintf("%d", pd.GitHub.Followers)) + githubTree.Child("Following: " + fmt.Sprintf("%d", pd.GitHub.Following)) + return githubTree +} + +func (pd *PersonData) TwitterTree() *tree.Tree { + twitterTree := tree.Root("Twitter") + twitterTree.Child("Handle: " + pd.Twitter.Handle) + twitterTree.Child("ID: " + pd.Twitter.ID) + twitterTree.Child("Bio: " + pd.Twitter.Bio) + twitterTree.Child("Followers: " + fmt.Sprintf("%d", pd.Twitter.Followers)) + twitterTree.Child("Following: " + fmt.Sprintf("%d", pd.Twitter.Following)) + twitterTree.Child("Location: " + pd.Twitter.Location) + twitterTree.Child("Site: " + pd.Twitter.Site) + twitterTree.Child("Avatar: " + pd.Twitter.Avatar) + return twitterTree +} + +func (pd *PersonData) LinkedInTree() *tree.Tree { + linkedinTree := tree.Root("LinkedIn") + linkedinTree.Child("Handle: " + pd.LinkedIn.Handle) + return linkedinTree +} + +func (pd *PersonData) GooglePlusTree() *tree.Tree { + googlePlusTree := tree.Root("GooglePlus") + googlePlusTree.Child("Handle: " + pd.GooglePlus.Handle) + return googlePlusTree +} + +func (pd *PersonData) GravatarTree() *tree.Tree { + gravatarTree := tree.Root("Gravatar") + gravatarTree.Child("Handle: " + pd.Gravatar.Handle) + gravatarTree.Child("Avatar: " + pd.Gravatar.Avatar) + return gravatarTree +} + +func (PersonData) TableName() string { + return "person" +} + +// PersonName contains the person's name components +type PersonName struct { + FullName string `json:"fullName"` + GivenName string `json:"givenName"` + FamilyName string `json:"familyName"` +} + +// PersonGeo contains location information for a person +type PersonGeo struct { + City string `json:"city"` + State string `json:"state"` + StateCode string `json:"stateCode"` + Country string `json:"country"` + CountryCode string `json:"countryCode"` + Lat float64 `json:"lat"` + Lng float64 `json:"lng"` +} + +// Employment contains employment information +type Employment struct { + Domain string `json:"domain"` + Name string `json:"name"` + Title string `json:"title"` + Role string `json:"role"` + SubRole string `json:"subRole"` + Seniority string `json:"seniority"` +} + +// GitHub contains GitHub profile information +type GitHub struct { + gorm.Model + Handle string `json:"handle"` + ID string `json:"id"` + Avatar string `json:"avatar"` + Company string `json:"company"` + Blog string `json:"blog"` + Followers int `json:"followers"` + Following int `json:"following"` + PersonID int `json:"person_id,omitempty"` +} + +// GooglePlus contains Google+ profile information +type GooglePlus struct { + Handle string `json:"handle"` + PersonID int `json:"person_id,omitempty"` +} + +// Gravatar contains Gravatar profile information +type Gravatar struct { + gorm.Model + Handle string `json:"handle"` + URLs []string `json:"urls" gorm:"serializer:json"` + Avatar string `json:"avatar"` + Avatars []string `json:"avatars" gorm:"serializer:json"` + PersonID int `json:"person_id,omitempty"` +} + +// Facebook contains Facebook profile information +type Facebook struct { + Handle string `json:"handle"` + Likes int `json:"likes"` +} + +// LinkedIn contains LinkedIn profile information +type LinkedIn struct { + Handle string `json:"handle"` +} + +// Twitter contains Twitter profile information +type Twitter struct { + Handle string `json:"handle"` + ID string `json:"id"` + Bio string `json:"bio"` + Followers int `json:"followers"` + Following int `json:"following"` + Location string `json:"location"` + Site string `json:"site"` + Avatar string `json:"avatar"` +} + +// Crunchbase contains Crunchbase profile information +type Crunchbase struct { + Handle string `json:"handle"` +} + +// YouTube contains YouTube profile information +type YouTube struct { + Handle string `json:"handle"` +} + +// PersonMeta contains metadata about the API response +type PersonMeta struct { + Email string `json:"email"` +} + +// HunterCombinedEnrichmentResponse represents the response from Hunter.io combined enrichment API +type HunterCombinedEnrichmentResponse struct { + Data CombinedData `json:"data" gorm:"embedded;embeddedPrefix:data_"` + Meta CombinedMeta `json:"meta" gorm:"embedded;embeddedPrefix:meta_"` +} + +// CombinedData contains both person and company information +type CombinedData struct { + Person PersonData `json:"person" gorm:"embedded;embeddedPrefix:person_"` + Company CompanyData `json:"company" gorm:"embedded;embeddedPrefix:company_"` +} + +func (cbd *CombinedData) String() string { + return fmt.Sprintf("Person: %s\nCompany: %s", + cbd.Person.String(), + cbd.Company.String()) +} + +// CombinedMeta contains metadata about the API response +type CombinedMeta struct { + Email string `json:"email"` +} + +// String returns a string representation of the combined enrichment response +func (c *HunterCombinedEnrichmentResponse) String() string { + return fmt.Sprintf("Person:\n%s\n\nCompany:\n%s", + c.Data.Person.String(), + c.Data.Company.String()) +} diff --git a/internal/sqlite/tables.go b/internal/sqlite/tables.go index f1a96b0..0e4493b 100644 --- a/internal/sqlite/tables.go +++ b/internal/sqlite/tables.go @@ -12,6 +12,8 @@ const ( SubdomainsTable HistoryTable LookupTable + HunterDomainTable + HunterEmailTable UnknownTable ) @@ -31,6 +33,10 @@ func GetTable(userInput string) Table { return HistoryTable case "lookup": return LookupTable + case "hunter_domain": + return HunterDomainTable + case "hunter_email": + return HunterEmailTable default: return UnknownTable } @@ -52,6 +58,10 @@ func (t Table) Object() interface{} { return HistoryRecord{} case LookupTable: return LookupResult{} + case HunterDomainTable: + return HunterDomainData{} + case HunterEmailTable: + return HunterEmail{} default: return nil } diff --git a/internal/sqlite/whois.go b/internal/sqlite/whois.go index 886ab62..d5b0f3f 100644 --- a/internal/sqlite/whois.go +++ b/internal/sqlite/whois.go @@ -50,7 +50,7 @@ func (w WhoisRecord) String() string { sb.WriteString(fmt.Sprintf("Domain Name Ext: %s\n", w.DomainNameExt)) sb.WriteString(fmt.Sprintf("Registrar Name: %s\n", w.RegistrarName)) sb.WriteString(fmt.Sprintf("Registrar IANA ID: %s\n", w.RegistrarIANAID)) - sb.WriteString(fmt.Sprintf("Contact Email: %s\n", w.ContactEmail)) + sb.WriteString(fmt.Sprintf("Contact HunterEmail: %s\n", w.ContactEmail)) sb.WriteString(fmt.Sprintf("Estimated Domain Age: %d days\n", w.EstimatedDomainAge)) // Dates @@ -379,7 +379,7 @@ func formatContact(sb *strings.Builder, contact ContactInfo, indent string) { sb.WriteString(indent + "Organization: " + contact.Organization + "\n") } if contact.Email != "" { - sb.WriteString(indent + "Email: " + contact.Email + "\n") + sb.WriteString(indent + "HunterEmail: " + contact.Email + "\n") } if contact.Street != "" { sb.WriteString(indent + "Street: " + contact.Street + "\n")