├── .gitignore ├── .DS_Store ├── gdrive_config.json ├── LICENSE ├── README.md └── Nimrod.rb /.gitignore: -------------------------------------------------------------------------------- 1 | /google_drive_config.json 2 | /bin.rb 3 | -------------------------------------------------------------------------------- /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomiwaAdey/Nimrod/HEAD/.DS_Store -------------------------------------------------------------------------------- /gdrive_config.json: -------------------------------------------------------------------------------- 1 | { 2 | "client_id": "xxxxxxxxxxxx-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx.apps.googleusercontent.com", 3 | "client_secret": "xxxxxxxxxxxxxxxxxxxxxxxx" 4 | } 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Tomiwa Adey 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Welcome to Nimrod 2 | Nimrod is a lead generation bot that helps you find the emails of people you want to reach out to. 3 | 4 | You hand Nimrod a google spreadsheet with the details of your prospects, and it fills in their emails from several tools for you. 5 | 6 | A more detailed explanation of Nimrod processes and how it works can be found here: 7 | [http://www.scrappycabin.com/blogs/the-nimrod-recipe](http://www.scrappycabin.com/blogs/the-nimrod-recipe) 8 | 9 | In short, Nimrod turns this: 10 | ![Prospect spreadsheet](https://lh5.googleusercontent.com/I5LAxRDbAz4-CwaWKl-vQBLHsqVrqvpN8dveCfymVuDXj17hcKdnv7GaJwFyvwQJBERJpAUFgkvwnCc_UopUgyNgcSWd4I0UEAdEP3VDPaBlLRDBvVTgBbkxev0jBLv6XTqIJTQJ) 11 | 12 | Into this: 13 | ![Completed prospect spreadsheet](https://lh4.googleusercontent.com/88kQKT5FYiqzahwBY_Bfl0i0OJKgF4YBYzQhhfKvbUUSGFqrsrHtVKsgqnvDUI91Ts40rZ0fxkOKle-Lb6NRYl7i3S09PimpWrVPTzQYKi_z2VTBjvqUZdxx9gF738qWSjSv0KyZ) 14 | 15 | ##How does Nimrod do his job? 16 | - Nimrod starts at the spreadsheet, it signs into your google drive and opens the spreadsheet you’ve specified. 17 | - For every prospect in your spreadsheet, it gets their name and domain. 18 | - It visits several tools one-by-one, on your behalf, enters the name and domain in the tool and fills the spreadsheet with the email suggestion returned. 19 | - It continues until it has exhausted all the prospects in the spreadsheet. 20 | - It hands you a completed spreadsheet. 21 | - It stops and it’s eager to get more work. 22 | 23 | > Nimrod can work 24hrs a day every day, it doesn’t complain, it doesn’t make mistakes, and it doesn’t ask you for money. 24 | 25 | Find out more about how Nimrod works here: [http://www.scrappycabin.com/blogs/the-nimrod-recipe](http://www.scrappycabin.com/blogs/the-nimrod-recipe) 26 | 27 | 28 | ## Setup for non-techies 29 | 30 | [Follow the guide here.](http://www.scrappycabin.com/blogs/the-nimrod-recipe#a3) 31 | 32 | 33 | ## Setup 34 | 35 | * To get started, get a list of prospects in a google spreadsheet. [Click here to get a sample template](https://docs.google.com/spreadsheets/u/2/d/1NTHbSxdr9PMKXf-wpWwaT44FR8YuLqRMPrURWwvaGC0/edit). It’s important that you do not change the format of the columns 36 | * Clone this repo in a local folder 37 | ### If you are using mac, you will already have ruby pre-installed on your machine 38 | * Run these commands in terminal: 39 | ``` 40 | gem install google_drive 41 | brew install phantomjs 42 | gem install watir-webdriver 43 | ``` 44 | * If you don't have Homebrew installed, run: 45 | ``` 46 | ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)" 47 | ``` 48 | ### If you are using windows: 49 | * Install Ruby with [Ruby Installer](https://rubyinstaller.org/downloads/) 50 | * Install [DevKit](http://rubyinstaller.org/add-ons/devkit/) 51 | * Install [phantomjs](https://bitbucket.org/ariya/phantomjs/downloads/phantomjs-2.1.1-windows.zip) 52 | * run `gem install watir-webdriver` 53 | * Then run `gem install google_drive` 54 | ### Now that the setup is done, 55 | * Navigate to the Nimrod folder and open it in your favourite code editor 56 | * Fill in your details in `@your_config` 57 | * The google spreadsheet keys can be found in the URL. It’s the alphanumeric keys between `d/` and `/edit` in the URL 58 | * Open the `gdrive_config.json` file and enter your google `client_id` & `client_secret`. [Follow this guide to get them](http://www.scrappycabin.com/guides/google-drive-authorisation/) 59 | * You’re good to go. Run: 60 | ``` 61 | ruby Nimrod.rb & 62 | ``` 63 | 64 | ## Would you like a web version of Nimrod? 65 | It’ll look something like this: 66 | ![Prospect spreadsheet](https://lh3.googleusercontent.com/Dvyzo2Pos3q9hGHmVGvpBZNsmkfxO3EkTtrkzCF-u4J3yo6_ry1Rk_T_XkWLGqEGp1X-9SiPPgshY81jeeyYrqmfQCBqHZ1aQNN2LO8b9twzz_DEKaz-tjEC9H7_Rd6XI-rRRjRZ) 67 | 68 | [Find out how to get it for free here.](http://www.scrappycabin.com/blogs/the-nimrod-recipe#blog-action-step) 69 | 70 | ### Report a bug & Contact 71 | 72 | If you'd like to report a bug, or if you have any questions or feedback, get in touch via [tomiwa@scrappycabin.com](mailto:tomiwa@scrappycabin.com). 73 |
74 | [Facebook](https://facebook.com/tomiwaAdey) [Twitter](https://twitter.com/tomiwaAdey) 75 | -------------------------------------------------------------------------------- /Nimrod.rb: -------------------------------------------------------------------------------- 1 | #Nimrod 2 | #========================================== 3 | # Author: Tomiwa Adey 4 | # Date: 25th March 2017 5 | # Title: Nimrod 6 | # Description: Nimrod is a bot that you can 7 | # employ to help you find the emails of 8 | # people you want to reach out to. 9 | # It does the research manually by signing into 10 | # several tools on your behalf and filling in 11 | # the details of the prospects one-by-one. 12 | # You hand Nimrod a spreadsheet with the details 13 | # of your prospects, and it fills in their 14 | # emails for you. 15 | # Nimrod can work 24 hrs a day every day, 16 | # It doesn't complain, it doesn't make mistakes, 17 | # and it doesn't ask you for money. 18 | #========================================== 19 | 20 | require "google_drive" 21 | require "watir-webdriver" 22 | #require "timeout" 23 | 24 | #Your config - Give nimrod the details it needs 25 | @your_config = { 26 | email_hunter_email: "", 27 | email_hunter_password: "", 28 | voila_norbert_email: "", 29 | voila_norbert_password: "", 30 | find_that_email_email: "", 31 | find_that_email_password: "", 32 | google_sheet_key: "", 33 | } 34 | 35 | #Email Hunter config 36 | @email_hunter = { 37 | url: "https://hunter.io/users/sign_in", 38 | email_field: "user[email]", 39 | password_field: "user[password]", 40 | login_button: "Log in »", 41 | website_field: 'domain-field', 42 | button: 'search-btn', 43 | pattern_result_div: '/html/body/div[3]/div/div[2]/div[1]/div[1]/div/div/div[1]/div', 44 | email_result_div: '.search.index .search-results .result .email' 45 | } 46 | 47 | #Voila Norbert config 48 | @voila_norbert = { 49 | url: "https://app.voilanorbert.com/#!/auth/login", 50 | email_field: "email", 51 | password_field: "password", 52 | login_button: "Let's do this, Norbert!", 53 | name_field: 'name', 54 | website_field: 'domain', 55 | button: 'Go ahead, Norbert!', 56 | email_result_div: '.contact-list' 57 | } 58 | 59 | #Find That Email config 60 | @find_that_email = { 61 | url: "https://findthat.email/sign-in/", 62 | email_field: "email", 63 | password_field: "password", 64 | login_button: "t_sign_submit", 65 | first_name_field: 'first_name', 66 | last_name_field: 'last_name', 67 | website_field: 'company_domain', 68 | button: 't_home_sumbit', 69 | answer_div: '//*[@id="t_d_search_log_box"]/div/div[1]/div' 70 | } 71 | 72 | 73 | 74 | # Creates a session. This will prompt the credential via command line for the 75 | # first time and save it to config.json file for later usages. 76 | session = GoogleDrive::Session.from_config("gdrive_config.json") 77 | 78 | # Go to spreadsheet 79 | ws = session.spreadsheet_by_key(@your_config[:google_sheet_key]).worksheets[0] 80 | 81 | 82 | # To do 83 | #replace sleeps with wait timeouts 84 | #https://ruby-doc.org/stdlib-2.4.1/libdoc/timeout/rdoc/Timeout.html 85 | 86 | def open_browser 87 | #open new browser 88 | browser = Watir::Browser.new :phantomjs 89 | 90 | #maximize the browser 91 | browser.driver.manage.window.maximize 92 | 93 | #return browser 94 | return browser 95 | end 96 | 97 | 98 | 99 | 100 | def email_hunter(browser, first_name, last_name, domain) 101 | return "Email Hunter login details missing." if @your_config[:email_hunter_email].nil? || @your_config[:email_hunter_password].nil? 102 | browser.goto(@email_hunter[:url]) 103 | sleep 10 104 | browser.text_field(:name => @email_hunter[:email_field]).set @your_config[:email_hunter_email] 105 | browser.text_field(:name => @email_hunter[:password_field]).set @your_config[:email_hunter_password] 106 | browser.button(:value => @email_hunter[:login_button]).click 107 | sleep 30 #wait for it to set the session 108 | browser.text_field(:id => @email_hunter[:website_field]).set domain 109 | browser.button(:id => @email_hunter[:button]).click 110 | sleep 20 111 | 112 | #Get email pattern 113 | #Replace the pattern with details you have 114 | if browser.div(:xpath => @email_hunter[:pattern_result_div]).exists? 115 | pattern = browser.div(:xpath => @email_hunter[:pattern_result_div]).text.split('Most common pattern: ')[-1] 116 | pattern_replacement = pattern.gsub('{first}', first_name.downcase).gsub('{f}', first_name[0].downcase).gsub('{last}', last_name.downcase).gsub('{l}', last_name[0].downcase) 117 | else 118 | pattern = 'Not found' 119 | pattern_replacement = 'Not found' 120 | end 121 | 122 | #Get email returned 123 | if browser.div(:css => @email_hunter[:email_result_div]).exists? 124 | email_gotten = browser.div(:css => @email_hunter[:email_result_div]).text 125 | else 126 | email_gotten = 'Not found' 127 | end 128 | 129 | #puts pattern + pattern_replacement + puts email_gotten 130 | return "Pattern: #{pattern}" + "\n" + "Pattern replacement: #{pattern_replacement}" + "\n" + "Email from source: #{email_gotten}" 131 | end 132 | 133 | def voila_norbert(browser, first_name, last_name, domain) 134 | return "Voila Norbert login details missing" if @your_config[:voila_norbert_email].nil? || @your_config[:voila_norbert_password].nil? 135 | browser.goto(@voila_norbert[:url]) 136 | sleep 10 137 | browser.text_field(:name => @voila_norbert[:email_field]).set @your_config[:voila_norbert_email] 138 | browser.text_field(:name => @voila_norbert[:password_field]).set @your_config[:voila_norbert_password] 139 | browser.element(:css => "input[type=submit]").click 140 | sleep 30 #wait for it to set the session 141 | browser.text_field(:name => @voila_norbert[:name_field]).set "#{first_name}" + ' ' + "#{last_name}" 142 | browser.text_field(:name => @voila_norbert[:website_field]).set domain 143 | browser.button(:value => @voila_norbert[:button]).click 144 | sleep 40 145 | contact_list = browser.ul(:css => @voila_norbert[:email_result_div]) 146 | contact_list.lis.each do |li| 147 | if li.text.include?("#{first_name}" + ' ' + "#{last_name}") 148 | return li.text 149 | break 150 | end 151 | end 152 | return "Not found" 153 | end 154 | 155 | def find_that_email(browser, first_name, last_name, domain) 156 | return "Find that email login details missing" if @your_config[:find_that_email_email].nil? || @your_config[:find_that_email_password].nil? 157 | browser.goto(@find_that_email[:url]) 158 | browser.text_field(:name => @find_that_email[:email_field]).set @your_config[:find_that_email_email] 159 | browser.text_field(:name => @find_that_email[:password_field]).set @your_config[:find_that_email_password] 160 | browser.execute_script('$("#t_sign_submit").click()') 161 | sleep 30 162 | browser.text_field(:name => @find_that_email[:first_name_field]).set first_name 163 | browser.text_field(:name => @find_that_email[:last_name_field]).set last_name 164 | browser.text_field(:name => @find_that_email[:website_field]).set domain #Can't use xpath because id changes 165 | sleep 5 166 | browser.execute_script('$("#t_my_app_add_button").click()') 167 | sleep 30 168 | if browser.div(:xpath=>@find_that_email[:answer_div]).exists? 169 | return browser.div(:xpath=>@find_that_email[:answer_div]).text.strip 170 | else 171 | return '' 172 | end 173 | 174 | end 175 | 176 | def whois(browser, first_name, last_name, domain) 177 | browser.goto('https://www.whoisxmlapi.com/?domainName=' + domain + '&outputFormat=xml') 178 | sleep 30 179 | content = browser.div(:id => 'wa-tab-content-whoislookup').text 180 | r = Regexp.new(/\b[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,4}\b/) 181 | emails = content.scan(r).uniq 182 | unless emails.nil? 183 | return emails.first 184 | else 185 | return 'Not Found' 186 | end 187 | end 188 | 189 | 190 | def with_error_handling 191 | yield 192 | rescue => e 193 | return e 194 | end 195 | 196 | 197 | #Main function 198 | # Get all rows in the spreadsheet 199 | (2..ws.num_rows).each do |row| 200 | #Next row in spreadsheet if first name, last name or domain is missing in the row 201 | next if ws[row, 1].nil? || ws[row, 2].nil? || ws[row, 3].nil? 202 | 203 | browser = open_browser 204 | 205 | #Step 1 email hunter 206 | email_hunter_result = with_error_handling { email_hunter(browser, ws[row, 1], ws[row, 2], ws[row, 3]) } 207 | #Enter email hunter result into spreadsheet 208 | ws[row, 4] = email_hunter_result 209 | 210 | #Step 2 Voila Norbert 211 | voila_norbert_results = with_error_handling { voila_norbert(browser, ws[row, 1], ws[row, 2], ws[row, 3]) } 212 | ws[row, 5] = voila_norbert_results 213 | 214 | #Step 3 Find That Email 215 | find_that_email_result = with_error_handling { find_that_email(browser, ws[row, 1], ws[row, 2], ws[row, 3]) } 216 | ws[row, 6] = find_that_email_result 217 | 218 | 219 | #Step 4 whois 220 | whois_result = with_error_handling { whois(browser, ws[row, 1], ws[row, 2], ws[row, 3]) } 221 | ws[row, 7] = whois_result 222 | 223 | ws.save 224 | browser.close 225 | sleep 10 226 | end 227 | --------------------------------------------------------------------------------