Skip to main content
The Business Outreach feature automates B2B lead generation and cold email campaigns. Scrape business information from Google Maps based on your niche, extract contact emails, and send personalized outreach messages.

Overview

MoneyPrinter V2’s outreach automation handles the entire lead generation pipeline:
1

Niche Definition

Specify the type of businesses you want to reach (e.g., “restaurants in New York”, “law firms in California”)
2

Google Maps Scraping

Uses a Go-based scraper to extract business names, websites, and contact info
3

Email Extraction

Visits each business website to find email addresses
4

Personalized Emails

Sends customized outreach messages with company name substitution

Setup Requirements

System Dependencies

Go Programming Language

Required to build the Google Maps scraper. Install from golang.org
sudo apt update
sudo apt install golang-go

Email Configuration

In config.json, configure SMTP settings:
{
  "email_credentials": {
    "username": "[email protected]",
    "password": "your-app-password",
    "smtp_server": "smtp.gmail.com",
    "smtp_port": 587
  },
  "google_maps_scraper_niche": "coffee shops in San Francisco",
  "google_maps_scraper_zip_url": "https://github.com/gosom/google-maps-scraper/archive/refs/heads/main.zip",
  "scraper_timeout": 300,
  "outreach_message_subject": "Boost {{COMPANY_NAME}}'s Online Presence",
  "outreach_message_body_file": "assets/outreach_template.txt"
}
email_credentials.username
string
required
Your email address for sending outreach emails
email_credentials.password
string
required
App-specific password (for Gmail, generate at myaccount.google.com/apppasswords)
smtp_server
string
default:"smtp.gmail.com"
SMTP server address for your email provider
smtp_port
integer
default:"587"
SMTP port (typically 587 for TLS, 465 for SSL)
google_maps_scraper_niche
string
required
Search query for Google Maps (e.g., “dentists in Austin, TX”)
scraper_timeout
integer
default:"300"
Maximum seconds to run the scraper (longer = more results)

Email Template

Create a message template at assets/outreach_template.txt:
Hi {{COMPANY_NAME}} team,

I noticed your business online and wanted to reach out about a service that could help increase your visibility and attract more customers.

We specialize in [your service offering] and have helped businesses like yours achieve [specific results].

Would you be interested in a quick 15-minute call to discuss how we can help {{COMPANY_NAME}}?

Best regards,
Your Name
Your Company
{{COMPANY_NAME}} is automatically replaced with each business’s name from the scraped data.

Scraper Setup

The first time you run outreach, MoneyPrinter V2 automatically:
1

Download Scraper

Fetches the Google Maps scraper from GitHub
2

Extract Archive

Unzips the scraper to the working directory
3

Build Binary

Compiles the Go application into an executable
4

Verify Installation

Checks that the binary was created successfully
def unzip_file(self, zip_link: str) -> None:
    if self._find_scraper_dir():
        info("=> Scraper already unzipped. Skipping unzip.")
        return

    r = requests.get(zip_link)
    z = zipfile.ZipFile(io.BytesIO(r.content))
    for member in z.namelist():
        if ".." in member or member.startswith("/"):
            warning(f"Skipping suspicious path in archive: {member}")
            continue
        z.extract(member)

Running an Outreach Campaign

From the main menu, select Outreach:
Select an option: 4
Starting Outreach...

=> Running scraper...
[Scraper output...]
=> Scraper finished successfully.

=> Scraped 47 items.

=> Setting email [email protected] for website https://coffeeshop1.com
=> Sending email to [email protected]...
=> Sent email to [email protected]

=> Setting email [email protected] for website https://coffeeshop2.com
=> Sending email to [email protected]...
=> Sent email to [email protected]

[... continues for all businesses ...]

Scraping Process

Niche File Creation

def start(self) -> None:
    # Check if go is installed
    if not self.is_go_installed():
        error("Go is not installed. Please install go and try again.")
        return

    # Unzip the scraper
    self.unzip_file(get_google_maps_scraper_zip_url())

    # Build the scraper
    self.build_scraper()

    # Write the niche to a file
    with open("niche.txt", "w") as f:
        f.write(self.niche)

    output_path = get_results_cache_path()
    message_subject = get_outreach_message_subject()
    message_body = get_outreach_message_body_file()

    # Run
    self.run_scraper_with_args_for_30_seconds(
        f'-input niche.txt -results "{output_path}"', timeout=get_scraper_timeout()
    )

Scraper Execution

def run_scraper_with_args_for_30_seconds(self, args: str, timeout=300) -> None:
    info(" => Running scraper...")
    binary_name = (
        "google-maps-scraper.exe"
        if platform.system() == "Windows"
        else "google-maps-scraper"
    )
    command = [os.path.join(os.getcwd(), binary_name)] + shlex.split(args)
    try:
        scraper_process = subprocess.run(command, timeout=float(timeout))

        if scraper_process.returncode == 0:
            print(colored("=> Scraper finished successfully.", "green"))
        else:
            print(colored("=> Scraper finished with an error.", "red"))
    except subprocess.TimeoutExpired:
        print(colored("=> Scraper timed out.", "red"))
    except Exception as e:
        print(colored("An error occurred while running the scraper:", "red"))
        print(str(e))
The scraper runs for the configured timeout (default 300 seconds = 5 minutes) and outputs results to .mp/cache/results.csv.

Output Format

Scraper results are CSV-formatted:
Name,Website
Blue Bottle Coffee,https://bluebottlecoffee.com
Philz Coffee,https://philzcoffee.com
Sightglass Coffee,https://sightglasscoffee.com

Email Extraction

For each business with a website, the system attempts to extract an email address:
def set_email_for_website(self, index: int, website: str, output_file: str):
    # Extract and set an email for a website
    email = ""

    r = requests.get(website)
    if r.status_code == 200:
        # Define a regular expression pattern to match email addresses
        email_pattern = r"\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,7}\b"

        # Find all email addresses in the HTML string
        email_addresses = re.findall(email_pattern, r.text)

        email = email_addresses[0] if len(email_addresses) > 0 else ""

    if email:
        print(f"=> Setting email {email} for website {website}")
        with open(output_file, "r", newline="", errors="ignore") as csvfile:
            csvreader = csv.reader(csvfile)
            items = list(csvreader)
            items[index].append(email)

        with open(output_file, "w", newline="", errors="ignore") as csvfile:
            csvwriter = csv.writer(csvfile)
            csvwriter.writerows(items)

Extraction Logic

1

HTTP Request

Sends GET request to the business website
2

Regex Matching

Searches HTML content for email patterns (e.g., [email protected])
3

First Match

Uses the first email found (typically a contact or info email)
4

CSV Update

Appends the email to the business’s row in the results file
Email extraction is best-effort. Many businesses hide emails behind contact forms, which won’t be detected.

Email Sending

SMTP Connection

# Create a yagmail SMTP client outside the loop
yag = yagmail.SMTP(
    user=self.email_creds["username"],
    password=self.email_creds["password"],
    host=self.email_creds["smtp_server"],
    port=self.email_creds["smtp_port"],
)
A single SMTP connection is reused for all emails to improve performance.

Message Personalization

for index, item in enumerate(items, start=1):
    try:
        # Check if the item's website is valid
        website = item.split(",")
        website = [w for w in website if w.startswith("http")]
        website = website[0] if len(website) > 0 else ""
        if website != "":
            test_r = requests.get(website)
            if test_r.status_code == 200:
                self.set_email_for_website(index, website, output_path)

                # Send emails using the existing SMTP connection
                receiver_email = item.split(",")[-1]

                if "@" not in receiver_email:
                    warning(f" => No email provided. Skipping...")
                    continue

                company_name = item.split(",")[0]
                subject = message_subject.replace(
                    "{{COMPANY_NAME}}", company_name
                )
                body = (
                    open(message_body, "r")
                    .read()
                    .replace("{{COMPANY_NAME}}", company_name)
                )

                info(f" => Sending email to {receiver_email}...")

                yag.send(
                    to=receiver_email,
                    subject=subject,
                    contents=body,
                )

                success(f" => Sent email to {receiver_email}")

Personalization Variables

{{COMPANY_NAME}}
string
Replaced with the scraped business name in both subject and body

Error Handling

The outreach process includes comprehensive error handling:

Website Validation

Verifies websites return 200 status before attempting email extraction

Email Validation

Checks for @ symbol before attempting to send

Exception Catching

Individual email failures don’t stop the entire campaign

Verbose Logging

Detailed logs help debug issues with specific businesses

Troubleshooting

Solution: Install Go from golang.org/dlVerify installation:
go version
# Should output: go version go1.xx.x ...
Causes:
  • Niche query is too specific or misspelled
  • Google Maps rate limiting
  • Network connectivity issues
Solutions:
  1. Broaden your niche (“coffee shops California” vs. “specialty roasters in San Francisco Financial District”)
  2. Increase scraper_timeout to allow more scraping time
  3. Check internet connection and firewall settings
Causes:
  • Businesses use contact forms instead of displaying emails
  • Websites protect emails (e.g., contact [at] example [dot] com)
  • JavaScript-rendered email addresses
Solutions:
  1. Manually find emails for high-value targets
  2. Use services like Hunter.io for email enrichment
  3. Modify regex pattern in Outreach.py:181 for obfuscated formats
For Gmail users:
  1. Enable 2-factor authentication
  2. Generate an App Password at myaccount.google.com/apppasswords
  3. Use the App Password (not your regular password) in config.json
For other providers:
  1. Verify SMTP server and port are correct
  2. Check if your provider requires SSL (port 465) or TLS (port 587)
  3. Ensure “less secure app access” is enabled if required
Solutions:
  1. Warm up your email address by sending low volumes initially
  2. Add SPF and DKIM records to your domain
  3. Personalize messages more (avoid generic templates)
  4. Include an unsubscribe option
  5. Use a professional email domain (not Gmail) for business outreach
Cause: Timeout too short for the number of results Google Maps needs to load.Solution: Increase timeout in config.json:
"scraper_timeout": 600  // 10 minutes instead of 5

Best Practices

Niche Targeting

Be specific but not overly restrictive. “Yoga studios in Brooklyn” > “businesses in New York”

Message Quality

Personalize beyond company name. Research your target niche’s pain points.

Volume Control

Start with 10-20 emails per day to avoid spam flags. Scale gradually.

Legal Compliance

Follow CAN-SPAM Act (US) and GDPR (EU) requirements:
  • Include physical address
  • Provide unsubscribe option
  • Honor opt-out requests within 10 days

Value Proposition

Lead with benefits to the recipient, not your service features.

Follow-up Strategy

Don’t spam. One follow-up after 3-5 days max.

Advanced Configuration

Custom Scraper Parameters

The scraper supports additional flags. Modify the command in Outreach.py:227:
self.run_scraper_with_args_for_30_seconds(
    f'-input niche.txt -results "{output_path}" -max 100 -depth 2',
    timeout=get_scraper_timeout()
)

Multiple Niche Campaigns

To run campaigns for different niches:
  1. Sequential Approach: Run outreach multiple times, updating google_maps_scraper_niche in config.json between runs
  2. Programmatic Approach (requires code modification):
    niches = ["dentists in Austin", "law firms in Boston", "restaurants in Miami"]
    
    for niche in niches:
        outreach = Outreach()
        outreach.niche = niche  # Override config
        outreach.start()
    

Email Rate Limiting

To avoid SMTP provider limits, add delays:
import time

# In Outreach.py, after yag.send():
time.sleep(2)  # 2-second delay between emails

Compliance and Ethics

Cold email outreach must comply with anti-spam laws:United States (CAN-SPAM Act):
  • Include accurate sender information
  • Use honest subject lines
  • Provide opt-out mechanism
  • Honor unsubscribe requests within 10 business days
European Union (GDPR):
  • Obtain consent before sending (or rely on legitimate interest)
  • Provide privacy policy
  • Allow data deletion requests
Canada (CASL):
  • Obtain express or implied consent
  • Include contact information
  • Provide unsubscribe mechanism
Violations can result in significant fines. Consult legal counsel before running large campaigns.

Results Tracking

Scraper results are saved to .mp/cache/results.csv. Track your campaign:
Name,Website,Email
Blue Bottle Coffee,https://bluebottlecoffee.com,[email protected]
Philz Coffee,https://philzcoffee.com,[email protected]
Sightglass Coffee,https://sightglasscoffee.com,[email protected]
For advanced tracking:
  1. Add UTM parameters to links in your email template
  2. Track opens using email marketing services
  3. Log responses manually or with email integration
  4. Calculate conversion rate: (Responses / Emails Sent) × 100

Source Code Reference

Build docs developers (and LLMs) love