19,475 Outreach Emails with 20 Lines of Code

Like most marketplaces, at Distribute we have a classic chicken/egg situation.

We’re on the lookout for great products and inventory and my recent focus was getting more suppliers to connect with our sales team.  

Below is the step-by-step method I used to send over 19,000 outreach emails to my exact target audience using a free forum’s messaging system.

Step 1: Consider your target customer.

WHO are our customers?

For Distribute, it’s simple. Our customers are retail buyers, quality brands, and suppliers in the USA. Sure, there are some pre-qualifying aspects as well, but that’s the gist.

WHERE are our customers?

Distribute customers are browsing eBay / Amazon and networking at industry conferences and online forums.

So I made a spreadsheet of the top wholesale/retail forums:

Step 2: Form a hypothesis

I’ve had a lot of previous success with cold email in the B2B context. So, after thoroughly researching the forums I decided to poke around and see if one would enable me to email all their members.

Hypothesis: There should be a way I can message wholesalers a relevant pitch about Distribute and convert them into users. 

Step 3: Run an initial experiment

Viewing profiles on TopTenWholesale requires being logged in, so I make a free account.

I’m in.

My profile is looking good too:
http://www.toptenwholesale.com/companies-hackerinc

Now let’s try sending a message.

(Note there’s no CAPTCHA. This is a critical ingredient to scaling private messaging on forums. If you are a forum manager reading this post, consider adding CAPTCHA-like technology to your private messaging system.)

After filling out the form quickly with fake data, here’s what happened:

Holy crap. My entire message, including the formatting, landed directly in the inbox.

Game on.

Step 4: Scale the experiment

At this point, without writing any code, or paying any money we’ve figured out how to send crispy messages to our target audience.

But, how do we get these messages sent in an efficient way and is there a way to get a list of all the Top Ten Wholesale profiles?

This is where it starts to get fun.

Investigate how TopTenWholesale.com handles private messaging

I’m a part-time student on Launch School (aff link), an online coding bootcamp. Two of their free resources are all you need to complete this hack:

  1. Intro to HTTP
  2. Intro to Ruby

Finished reading? Ok, let’s get to it.

In Google Chrome, I revisited the ‘send a message’ page and opened the Chrome console (shift+cmd+c on Mac, shift+ctrl+c on PC).

See that red dot on the left side? Make sure yours is red too, so that the console records all the interactions you’re making with the website’s server.

Note: this console may appear on the far right side of your Chrome browser, and not the bottom.

Next, I clicked ‘Send Message’ and was redirect to a confirmation page. This will come in handy later. Now it’s time to check the network activity.

Looks like gibberish, eh? No worries, just keep following along.

Each of these line items represents a request from our client, the Chrome browser, to the Top Ten Wholesale server.

As you can see, there’s a lot of Javascript and some styles, being fetched and then rendered on the thank you page.

Let’s sort the 300+ requests by their Method.

Now the ‘POST’ requests are at the top… this is typically the Method used by message forms, or object creation/deletion requests, etc, to process stuff on the backend of your favorite apps.

I started scrolling past a bunch of gibberish, and found this suspicious ‘app.cgi’ POST request. Let’s inspect that. Click. On the right side, you’ll notice a ‘Request Headers’ section. After scrolling down…

See the ‘form data’ bit? BINGO.

This exposes to us, the exact structure of the browser’s POST request to the Top Ten Wholesale server.

In theory, we can send POST requests on our own, from the command line, and achieve the same results.

Sending our first programmatic message

cURL is a free software tool that lets you fetch data from the internet, using your computer’s terminal as the client instead of Google Chrome, your phone, etc.

If you open your terminal, we can send cURL requests like this:

In the above example our terminal receives back all the same HTML/CSS content that a web browser receives upon visiting ryanckulp.com. The difference is that Chrome knows how to render that content into nice interfaces, and our terminal does not.

So now let’s create a cURL request that mirrors the one our browser made earlier, to remotely send a private message on Top Ten Wholesale.

To make this a little easier to read, I’ve created the Curl request in Ruby. You can learn enough Ruby from the free book linked above to understand this code.

The ‘params’ variable holds a set of keys and values. We found these keys in the Network tab > POST request > Request Headers data that we found earlier.

The next line, is doing a few things. The ‘Curl’ object is calling a POST request, and passing in a URL, as well as those params, to tell the POST request where to send data.

Note: none of this is guesswork, all the data is wide open in the Chrome Network tab.

Setting “resp” to contain the return value of this Curl method, lets me inspect the page content just like we did in the curl http://www.ryanckulp.com example above.

To execute this code I just typed irb in my terminal, then ‘enter’, then copy/pasted the code and hit ‘enter’ again.

After running the code, my inbox has another confirmation email (aka gift) from Top Ten Wholesale.

But wait a second. Something is very different about this message.

  • Blank “… sent you a message”, no name
  • “Liz Elswick” contact information… who the heck is Liz?!

Obviously this is no good. Even if we could get a bunch of profile URLs, it would still say Liz, and not have our name.

So I revisited the Network tab, and noticed a ‘Cookie’ value was set. This is basically a long ugly string, that is often used by servers and web browsers as a way to keep you logged into a website.

Rather than parse out the “useful” stuff, I simply copy/pasted the Cookie data and updated my Curl request.

(This isn’t the full Cookie string… I truncated for the sake of this screenshot, and my own account’s security.)

After executing the new code, the confirmation email includes our Jim Bob sender details instead of “Liz Elswick” (?).

Kickass, now we’re talking.

Mid Outreach-Hack Recap

Here’s what we just got done:

  • Research wholesaler forums
  • Organize a spreadsheet of prospects, pick a target
  • Sign up for a free account
  • Create a fake profile and send a test message
  • Examine my inbox, send another message, inspect network tab
  • Write a cURL request, using data from the Network tab logs

With this out of the way, I’m now ready to send a lot more messages.

The only thing I’m missing is the ‘site’ parameter of the cURL request, which, from pure observation, seems to be whatever comes before the ‘-profile’ in the URL bar on my profile.

To refresh your memory, here were the params I sent in my first programmatic message, that worked:

Notice the ‘hackerinc’ value for the ‘site’ parameter? Yeah, we want thousands of those, for every profile on the platform.

Finding all the wholesaler user profiles

The Internet has this great thing called Sitemaps. So I ran a Google search:

The first link merely listed direct links to a few of their categories, forums, features, etc. But the 5th link (/sitemaps/products_1.xml) was interesting. It showed me a bunch of direct links to products.

Taking a step back, I basically need something like this, but for company profiles. So, I swapped “products” with “companies” in the URL, and hit enter.

#jackpot.

Now we just need the profiles to be in a specific format:

  • No trailing URL, dashes, etc. Just the company vanity url, ie ‘outdoor’ (example above)
  • No duplicates. In the screenshot, you’ll notice 2 slightly different links for the ‘xtremcare’ company.

Since I’ve never parsed XML data before, yet-another Google search helped me figure it out.

This loads all the content from the webpage, into a variable called doc. Next I was able to handle my formatting specifications with Ruby 101.

The ‘companies’ variable is now loaded up with vanity slugs that I can use in the ‘site’ field for my cURL params.

Phew! Now for finishing touches.

Cleaning up my messaging

After taking these screenshots, I changed my profile account data to actual Distribute info. (Distribute is the startup I worked at)

With all the profiles, code, and tests in good shape, sending 19,475 emails was as simple as looping through each company profile, and passing in the vanity slug as a ‘site’ param.

I won’t paste that code, because if you’re intending to run an outreach hack like this, you should probably figure out this last step on your own.

But you get the idea of how an overall campaign like this can look like. We have some code that sends a cURL request to their server, emulating what the web browser does upon clicking ‘Send Message’, and the request header contains my long Cookie string to trick their server into believing I’m a ‘logged in’ user on the website.

To make sure I could see what was happening, I added some error handling and logging, ie “sent message successfully,” and “message failed at profile XYZ”, with the following:

(If you recall, after sending a message from the web we were taken to a confirmation page. Since the ‘resp’ variable contains all the HTML/CSS of that page, I’m simply scanning the document for the words “Your message has been sent to,” and using that as an indicator of success/failure.)

Kinda hacky, but it works.

Evaluating Results

Once my script was running, it sent 3-5 messages per second.

Within minutes my inbox began filling up with positive responses from potential wholesale users.

At the ~7,000 message mark, my free TopTenWholsale.com account got locked. I tried signing in, clicking ‘forgot password,’ nothing worked.

So I opened Chrome Incognito, and made a new account. After copy/pasting the looping code into my terminal (starting at the company where it broke), I was live again in within moments of the initial error.

Throughout the day, many more people replied, and the sales team had to get involved.

Wholesaler outreach, accomplished, with 20 lines of code.