← Back to DevBytes

Building a Email Automation Agent with LangChain: Complete Guide

What Is an Email Automation Agent with LangChain?

An Email Automation Agent built with LangChain is an AI-powered system that can read, compose, search, and manage emails autonomously—or with minimal human oversight. Rather than relying on rigid rule-based filters or pre-scripted workflows, this agent leverages a Large Language Model (LLM) to interpret the content and context of emails, decide what action to take, and execute it through a set of defined tools.

LangChain provides the orchestration layer: it connects the LLM's reasoning capabilities to concrete email operations such as fetching unread messages, extracting key information, drafting replies, scheduling follow-ups, and even triaging spam. The agent operates in a tool-calling loop—the LLM receives an observation (e.g., a new email), reasons about it, selects the appropriate tool, and processes the result until the task is complete.

In practice, this means you can build a system that:

Why It Matters

Email remains the backbone of business communication, yet the volume continues to grow relentlessly. Knowledge workers spend an estimated 28% of their workweek reading and responding to emails. An intelligent agent doesn't just save time—it fundamentally changes how you interact with your inbox by:

LangChain specifically matters here because it abstracts away the complexity of chaining LLM calls, managing tool selection, handling memory across conversations, and integrating with external APIs. Without it, you'd be writing custom orchestration logic that quickly becomes unmaintainable as the agent's capabilities grow.

How to Build an Email Automation Agent: Step-by-Step

1. Architecture Overview

Our agent follows a standard LangChain ReAct (Reason + Act) pattern. The components are:

2. Project Setup

Create a new Python project and install the required dependencies:

mkdir email-agent
cd email-agent
python -m venv .venv
source .venv/bin/activate  # On Windows: .venv\Scripts\activate

pip install langchain langchain-openai langchain-community
pip install google-api-python-client google-auth-httplib2 google-auth-oauthlib
pip install python-dotenv rich

Create a .env file for your secrets:

OPENAI_API_KEY=sk-your-openai-key-here
GMAIL_CREDENTIALS_FILE=credentials.json
GMAIL_TOKEN_FILE=token.json
AGENT_EMAIL=your-email@example.com

3. Setting Up Gmail API Access

To let the agent interact with Gmail programmatically, you need OAuth 2.0 credentials. Follow these steps:

Now build the Gmail service adapter:

# gmail_adapter.py
import os
import pickle
import base64
from email.mime.text import MIMEText
from google.auth.transport.requests import Request
from google_auth_oauthlib.flow import InstalledAppFlow
from googleapiclient.discovery import build

SCOPES = ['https://www.googleapis.com/auth/gmail.modify']

def get_gmail_service():
    """Authenticate and return the Gmail API service instance."""
    creds = None
    token_file = os.getenv('GMAIL_TOKEN_FILE', 'token.json')
    
    # Load cached credentials if they exist
    if os.path.exists(token_file):
        with open(token_file, 'rb') as token:
            creds = pickle.load(token)
    
    # Refresh or create credentials
    if not creds or not creds.valid:
        if creds and creds.expired and creds.refresh_token:
            creds.refresh(Request())
        else:
            flow = InstalledAppFlow.from_client_secrets_file(
                os.getenv('GMAIL_CREDENTIALS_FILE', 'credentials.json'), SCOPES
            )
            creds = flow.run_local_server(port=8080)
        # Save for future runs
        with open(token_file, 'wb') as token:
            pickle.dump(creds, token)
    
    return build('gmail', 'v1', credentials=creds)

4. Defining the Agent's Tools

Tools are the hands of your agent. Each tool is a Python function annotated with @tool decorator (or wrapped manually). The function's docstring becomes the tool description that the LLM uses to decide when and how to call it.

# tools.py
import base64
import email
from email.mime.text import MIMEText
from typing import List, Optional
from langchain.tools import tool
from gmail_adapter import get_gmail_service

@tool
def fetch_unread_emails(max_results: int = 5) -> str:
    """
    Fetch the most recent unread emails from the inbox.
    Returns a formatted string containing sender, subject, date, and body snippet for each email.
    Use this to check for new messages that need attention.
    
    Args:
        max_results: Number of unread emails to fetch (default 5, max 20)
    """
    service = get_gmail_service()
    max_results = min(max(1, max_results), 20)
    
    results = service.users().messages().list(
        userId='me',
        maxResults=max_results,
        q='is:unread',
        labelIds=['INBOX']
    ).execute()
    
    messages = results.get('messages', [])
    if not messages:
        return "No unread emails found."
    
    output_lines = []
    for msg in messages:
        detail = service.users().messages().get(
            userId='me', id=msg['id'], format='metadata',
            metadataHeaders=['From', 'Subject', 'Date']
        ).execute()
        
        headers = detail.get('payload', {}).get('headers', [])
        from_addr = next((h['value'] for h in headers if h['name'] == 'From'), 'Unknown')
        subject = next((h['value'] for h in headers if h['name'] == 'Subject'), '(No subject)')
        date_str = next((h['value'] for h in headers if h['name'] == 'Date'), 'Unknown')
        
        # Get a snippet of the body
        snippet = detail.get('snippet', '')[:200]
        
        output_lines.append(
            f"ID: {msg['id']}\n"
            f"From: {from_addr}\n"
            f"Subject: {subject}\n"
            f"Date: {date_str}\n"
            f"Snippet: {snippet}\n"
            f"---"
        )
    
    return "\n".join(output_lines)

@tool
def get_email_body(message_id: str) -> str:
    """
    Retrieve the full decoded body of a specific email by its message ID.
    Use this when you need to read an email's complete content before drafting a response.
    
    Args:
        message_id: The unique Gmail message ID (obtained from fetch_unread_emails)
    """
    service = get_gmail_service()
    detail = service.users().messages().get(
        userId='me', id=message_id, format='full'
    ).execute()
    
    payload = detail.get('payload', {})
    body_data = None
    
    # Handle multipart messages
    if 'parts' in payload:
        for part in payload['parts']:
            if part.get('mimeType') == 'text/plain':
                body_data = part.get('body', {}).get('data')
                break
        # Fallback to HTML part
        if not body_data:
            for part in payload['parts']:
                if part.get('mimeType') == 'text/html':
                    body_data = part.get('body', {}).get('data')
                    break
    else:
        body_data = payload.get('body', {}).get('data')
    
    if not body_data:
        return "(This email has no readable text body.)"
    
    decoded = base64.urlsafe_b64decode(body_data.encode('ASCII')).decode('utf-8', errors='replace')
    return decoded[:4000]  # Truncate very long emails

@tool
def search_emails(query: str, max_results: int = 10) -> str:
    """
    Search emails using Gmail's query syntax.
    Supports operators like 'from:', 'subject:', 'has:attachment', 'older_than:', etc.
    Use this to find specific conversations, threads, or archived messages.
    
    Args:
        query: Gmail search query string (e.g., "from:boss@company.com subject:budget")
        max_results: Maximum number of results (default 10, max 25)
    """
    service = get_gmail_service()
    max_results = min(max(1, max_results), 25)
    
    results = service.users().messages().list(
        userId='me', maxResults=max_results, q=query
    ).execute()
    
    messages = results.get('messages', [])
    if not messages:
        return f"No emails found matching query: '{query}'"
    
    output_lines = [f"Found {len(messages)} emails matching '{query}':\n"]
    for msg in messages[:max_results]:
        detail = service.users().messages().get(
            userId='me', id=msg['id'], format='metadata',
            metadataHeaders=['From', 'Subject', 'Date']
        ).execute()
        headers = detail.get('payload', {}).get('headers', [])
        from_addr = next((h['value'] for h in headers if h['name'] == 'From'), 'Unknown')
        subject = next((h['value'] for h in headers if h['name'] == 'Subject'), '(No subject)')
        output_lines.append(f"- [{msg['id']}] {from_addr} | {subject}")
    
    return "\n".join(output_lines)

@tool
def send_email(to: str, subject: str, body: str) -> str:
    """
    Compose and send an email from the authenticated account.
    Use this to reply to messages, send notifications, or follow up on conversations.
    
    Args:
        to: Recipient email address (e.g., 'john@example.com')
        subject: Email subject line (will be prefixed with 'Re: ' for replies if appropriate)
        body: Plain text email body, keep it professional and concise
    """
    service = get_gmail_service()
    message = MIMEText(body)
    message['to'] = to
    message['subject'] = subject
    message['from'] = os.getenv('AGENT_EMAIL', 'me')
    
    raw = base64.urlsafe_b64encode(message.as_bytes()).decode('ASCII')
    sent = service.users().messages().send(
        userId='me', body={'raw': raw}
    ).execute()
    
    return f"Email sent successfully to {to}. Message ID: {sent['id']}"

@tool
def apply_label(message_id: str, label_name: str) -> str:
    """
    Apply a Gmail label to a specific email message.
    Use this to categorize emails (e.g., 'Urgent', 'Follow-up', 'Done', 'Archive').
    Creates the label if it doesn't already exist.
    
    Args:
        message_id: The Gmail message ID
        label_name: The label to apply (e.g., 'Urgent', 'Follow-up')
    """
    service = get_gmail_service()
    
    # Find or create the label
    labels = service.users().labels().list(userId='me').execute().get('labels', [])
    label_id = None
    for lbl in labels:
        if lbl['name'].lower() == label_name.lower():
            label_id = lbl['id']
            break
    
    if not label_id:
        created = service.users().labels().create(
            userId='me',
            body={'name': label_name, 'labelListVisibility': 'labelShow', 'messageListVisibility': 'show'}
        ).execute()
        label_id = created['id']
    
    service.users().messages().modify(
        userId='me', id=message_id,
        body={'addLabelIds': [label_id]}
    ).execute()
    
    return f"Label '{label_name}' applied to message {message_id}"

@tool
def mark_as_read(message_id: str) -> str:
    """
    Mark a specific email as read (remove the UNREAD label).
    Use this after processing an email to prevent duplicate handling.
    
    Args:
        message_id: The Gmail message ID
    """
    service = get_gmail_service()
    service.users().messages().modify(
        userId='me', id=message_id,
        body={'removeLabelIds': ['UNREAD']}
    ).execute()
    return f"Message {message_id} marked as read."

5. Assembling the Agent

Now we wire everything together using LangChain's create_tool_calling_agent and AgentExecutor. This agent receives a natural language instruction, reasons about which tools to call, and iterates until it has completed the task.

# agent.py
import os
from dotenv import load_dotenv
from langchain_openai import ChatOpenAI
from langchain.agents import create_tool_calling_agent, AgentExecutor
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain_core.messages import SystemMessage, HumanMessage
from tools import (
    fetch_unread_emails,
    get_email_body,
    search_emails,
    send_email,
    apply_label,
    mark_as_read,
)

load_dotenv()

# The system prompt defines the agent's personality and operating constraints
SYSTEM_PROMPT = """You are an expert email management assistant with access to a Gmail inbox.
Your goal is to help the user stay on top of their email efficiently and professionally.

You have the following capabilities:
- Fetch unread emails and summarize them
- Read full email bodies when needed
- Search for specific emails using Gmail query syntax
- Send emails on behalf of the user
- Apply labels to categorize emails
- Mark emails as read after processing

Core operating principles:
1. When summarizing emails, be concise but capture the key point, sender, and any action required
2. Never send an email without first reading the full context of what you're replying to
3. Draft responses that are warm but professional; mirror the sender's tone where appropriate
4. If an email contains a clear question, answer it directly using your knowledge—but don't fabricate information
5. When unsure about something (e.g., calendar availability, confidential data), flag it for the user instead of guessing
6. Apply the 'Urgent' label to emails that appear time-sensitive (deadlines within 24h, crisis language)
7. Always mark emails as read after fully processing them
8. Log every action you take so the user can review later

You operate in a tool-calling loop: observe → reason → act → observe. Be efficient—avoid redundant tool calls."""

def create_email_agent():
    """Build and return the email automation agent."""
    
    llm = ChatOpenAI(
        model="gpt-4o",
        temperature=0.3,  # Lower temperature for reliable tool selection
        max_tokens=2000,
    )
    
    # All tools the agent can use
    tools = [
        fetch_unread_emails,
        get_email_body,
        search_emails,
        send_email,
        apply_label,
        mark_as_read,
    ]
    
    # Prompt template with system message and conversation history placeholders
    prompt = ChatPromptTemplate.from_messages([
        ("system", SYSTEM_PROMPT),
        ("human", "{input}"),
        MessagesPlaceholder(variable_name="agent_scratchpad"),
    ])
    
    agent = create_tool_calling_agent(llm, tools, prompt)
    
    executor = AgentExecutor(
        agent=agent,
        tools=tools,
        verbose=True,  # Set to True during development to see the reasoning
        max_iterations=8,
        max_execution_time=120,  # 2-minute timeout
        handle_parsing_errors=True,
        return_intermediate_steps=False,
    )
    
    return executor

# Example usage
if __name__ == "__main__":
    agent = create_email_agent()
    
    # Task 1: Morning briefing
    print("=== Morning Email Briefing ===")
    result = agent.invoke({
        "input": "Fetch my unread emails and give me a concise morning briefing. "
                 "Highlight any urgent items and suggest what needs my immediate attention."
    })
    print(result['output'])
    
    # Task 2: Handle a specific thread
    print("\n=== Handling Specific Thread ===")
    result = agent.invoke({
        "input": "Search for emails from 'alex@partnercompany.com' about the Q4 proposal. "
                 "Read the most recent one and draft a thoughtful reply confirming we're on track."
    })
    print(result['output'])

6. Adding Memory for Context-Aware Conversations

Without memory, each agent invocation is stateless. Adding LangChain's ConversationSummaryMemory allows the agent to remember what it did across sessions, which is invaluable for tracking ongoing email threads.

# agent_with_memory.py
from langchain.memory import ConversationSummaryMemory
from langchain_openai import ChatOpenAI

def create_email_agent_with_memory():
    """Build an agent that retains context across invocations."""
    
    llm = ChatOpenAI(model="gpt-4o", temperature=0.3)
    tools = [fetch_unread_emails, get_email_body, search_emails, 
             send_email, apply_label, mark_as_read]
    
    # Memory stores a running summary of past interactions
    memory = ConversationSummaryMemory(
        llm=llm,
        memory_key="chat_history",
        return_messages=True,
        max_token_limit=1000,
    )
    
    prompt = ChatPromptTemplate.from_messages([
        ("system", SYSTEM_PROMPT),
        MessagesPlaceholder(variable_name="chat_history"),
        ("human", "{input}"),
        MessagesPlaceholder(variable_name="agent_scratchpad"),
    ])
    
    agent = create_tool_calling_agent(llm, tools, prompt)
    
    executor = AgentExecutor(
        agent=agent,
        tools=tools,
        memory=memory,
        verbose=True,
        max_iterations=10,
        handle_parsing_errors=True,
    )
    
    return executor

# Interactive loop with memory
if __name__ == "__main__":
    agent = create_email_agent_with_memory()
    
    print("Email Agent ready. Type 'quit' to exit, 'reset' to clear memory.")
    while True:
        user_input = input("\nYou: ")
        if user_input.lower() == 'quit':
            break
        elif user_input.lower() == 'reset':
            agent.memory.clear()
            print("Memory cleared.")
            continue
        
        result = agent.invoke({"input": user_input})
        print(f"Agent: {result['output']}")

7. Creating a Scheduled Daily Briefing Service

A production-ready agent needs scheduling. Here's how to wrap the agent in a cron-friendly script that runs every morning at 7 AM:

# daily_briefing.py
import json
import os
from datetime import datetime
from agent import create_email_agent

def run_morning_briefing():
    """Run the full morning email briefing workflow."""
    agent = create_email_agent()
    
    # Step 1: Get unread emails
    result = agent.invoke({
        "input": (
            "Please do the following in order:\n"
            "1. Fetch all unread emails (up to 20)\n"
            "2. For each email, read the full body if the snippet suggests it needs action\n"
            "3. Categorize emails into: 'Urgent' (needs response within 24h), "
               "'Important' (needs response this week), 'Newsletter/Update' (just informational), "
               "and 'Spam/Noise' (can be archived)\n"
            "4. Apply appropriate labels to each email\n"
            "5. Mark all processed emails as read\n"
            "6. Finally, provide a structured briefing with: "
               "total count, urgent items with action required, "
               "and a suggested prioritization for my day"
        )
    })
    
    # Save briefing to a file for later reference
    briefing_file = f"briefings/briefing_{datetime.now().strftime('%Y-%m-%d')}.txt"
    os.makedirs("briefings", exist_ok=True)
    with open(briefing_file, 'w') as f:
        f.write(f"Email Briefing — {datetime.now().strftime('%A, %B %d %Y')}\n")
        f.write("=" * 50 + "\n\n")
        f.write(result['output'])
    
    print(f"Briefing saved to {briefing_file}")
    return result['output']

if __name__ == "__main__":
    briefing = run_morning_briefing()
    print(briefing)

Set up a cron job (Linux/macOS) or Task Scheduler (Windows) to run this script:

# Crontab entry (runs daily at 7:00 AM)
0 7 * * * cd /path/to/email-agent && .venv/bin/python daily_briefing.py >> logs/cron.log 2>&1

8. Advanced: Intelligent Reply Drafting with Context

The agent's true power emerges when it reads an entire thread, understands the nuance, and drafts a reply that matches the conversation's tone and content. Here's a specialized tool for thread-aware replies:

# advanced_tools.py
@tool
def draft_thread_reply(message_id: str, tone: str = "professional") -> str:
    """
    Read a full email thread and draft a contextually appropriate reply.
    Only drafts the reply—does NOT send it—so the user can review first.
    
    Args:
        message_id: The Gmail message ID to reply to
        tone: Desired tone ('professional', 'friendly', 'concise', 'formal')
    """
    service = get_gmail_service()
    
    # Get the full message including thread info
    detail = service.users().messages().get(
        userId='me', id=message_id, format='full'
    ).execute()
    
    # Extract thread ID
    thread_id = detail.get('threadId')
    
    # Fetch the entire thread
    thread = service.users().threads().get(
        userId='me', id=thread_id, format='full'
    ).execute()
    
    messages = thread.get('messages', [])
    thread_context = []
    
    for msg in messages:
        headers = msg.get('payload', {}).get('headers', [])
        from_addr = next((h['value'] for h in headers if h['name'] == 'From'), 'Unknown')
        date_str = next((h['value'] for h in headers if h['name'] == 'Date'), 'Unknown')
        
        # Decode body
        payload = msg.get('payload', {})
        body_data = None
        if 'parts' in payload:
            for part in payload['parts']:
                if part.get('mimeType') == 'text/plain':
                    body_data = part.get('body', {}).get('data')
                    break
        else:
            body_data = payload.get('body', {}).get('data')
        
        body_text = ""
        if body_data:
            body_text = base64.urlsafe_b64decode(body_data.encode('ASCII')).decode('utf-8', errors='replace')[:1000]
        
        thread_context.append({
            'from': from_addr,
            'date': date_str,
            'body': body_text
        })
    
    # Format thread for the LLM to process
    thread_text = "=== EMAIL THREAD ===\n\n"
    for i, msg in enumerate(thread_context, 1):
        thread_text += f"[Message {i}] From: {msg['from']} | Date: {msg['date']}\n"
        thread_text += f"{msg['body']}\n"
        thread_text += "---\n"
    
    # The agent will incorporate this context when drafting
    return (
        f"{thread_text}\n\n"
        f"=== DRAFTING INSTRUCTIONS ===\n"
        f"Based on the thread above, draft a {tone} reply to the most recent message. "
        f"Consider: the ongoing discussion, any unanswered questions, "
        f"the relationship dynamics visible in the thread, and appropriate next steps. "
        f"Return ONLY the draft email body—no subject line, no metadata. "
        f"The user will review and approve before sending."
    )

Best Practices for Production Email Agents

1. Implement a Human-in-the-Loop Safeguard

Never let the agent send emails without approval—especially for high-stakes communication. Implement a draft-then-review pattern where the agent saves drafts to a "Pending Review" label, and a human approves them via a simple CLI or web dashboard before they're sent.

# safegaurd pattern
@tool
def draft_email(to: str, subject: str, body: str) -> str:
    """Draft an email and save it with 'Pending Review' label. Does NOT send."""
    service = get_gmail_service()
    message = MIMEText(body)
    message['to'] = to
    message['subject'] = f"[DRAFT] {subject}"
    message['from'] = os.getenv('AGENT_EMAIL', 'me')
    
    raw = base64.urlsafe_b64encode(message.as_bytes()).decode('ASCII')
    draft = service.users().messages().send(
        userId='me', body={'raw': raw, 'labelIds': ['INBOX', 'DRAFT']}
    ).execute()
    
    apply_label(draft['id'], 'Pending Review')
    return f"Draft saved as message {draft['id']} with 'Pending Review' label. Awaiting human approval."

2. Set Strict Rate Limits and Budgets

Gmail API has quotas (typically 250 quota units per second per user). Beyond that, OpenAI and other LLM providers charge per token. Implement guardrails:

# rate_limiter.py
import time
from functools import wraps

def rate_limit(calls_per_minute: int = 30):
    """Decorator to rate-limit tool calls."""
    calls = []
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            now = time.time()
            # Remove calls older than 1 minute
            while calls and calls[0] < now - 60:
                calls.pop(0)
            if len(calls) >= calls_per_minute:
                wait_time = 60 - (now - calls[0])
                time.sleep(max(0, wait_time))
            calls.append(time.time())
            return func(*args, **kwargs)
        return wrapper
    return decorator

# Apply to heavy tools
@rate_limit(20)
@tool
def fetch_unread_emails(max_results: int = 5) -> str:
    # ... implementation

3. Engineer Prompts for Reliability

The system prompt is the single most important factor in agent quality. Key principles:

4. Log Everything Religiously

Every agent action should be logged with timestamps, inputs, and outputs. This is critical for debugging, auditing, and improving the agent over time.

# logging_setup.py
import logging
import json
from datetime import datetime

# Structured logging for agent actions
agent_logger = logging.getLogger('email_agent')
agent_logger.setLevel(logging.INFO)

# File handler with rotation
from logging.handlers import RotatingFileHandler
handler = RotatingFileHandler('logs/agent_actions.log', maxBytes=10_000_000, backupCount=5)
handler.setFormatter(logging.Formatter('%(asctime)s | %(levelname)s | %(message)s'))
agent_logger.addHandler(handler)

def log_tool_call(tool_name: str, args: dict, result: str):
    """Log every tool invocation with structured data."""
    agent_logger.info(json.dumps({
        'tool': tool_name,
        'args': args,
        'result_preview': result[:200],
        'timestamp': datetime.now().isoformat(),
    }))

# Wrap tools to auto-log
class LoggingToolWrapper:
    def __init__(self, tool_func):
        self.tool = tool_func
        self.name = tool_func.name if hasattr(tool_func, 'name') else tool_func.__name__
    
    def __call__(self, *args, **kwargs):
        result = self.tool(*args, **kwargs)
        log_tool_call(self.name, kwargs, str(result))
        return result

5. Handle Errors Gracefully

Network failures, API quota exhaustion, and malformed emails are inevitable. Your agent must degrade gracefully:

# error_handling.py
from tenacity import retry, stop_after_attempt, wait_exponential
import functools

def with_retry_and_fallback(fallback_message: str = "Operation failed after retries"):
    """Decorator that retries with exponential backoff and returns a fallback."""
    def decorator(func):
        @retry(
            stop=stop_after_attempt(3),
            wait=wait_exponential(multiplier=1, min=2, max=30),
            reraise=False,
        )
        def wrapper(*args, **kwargs):
            return func(*args, **kwargs)
        
        @functools.wraps(func)
        def safe_execution(*args, **kwargs):
            try:
                return wrapper(*args, **kwargs)
            except Exception as e:
                error_msg = f"{fallback_message}: {str(e)[:200]}"
                return error_msg
        return safe_execution
    return decorator

# Apply to API-facing tools
@with_retry_and_fallback("Could not fetch emails due to a temporary issue")
@tool
def fetch_unread_emails_safe(max_results: int = 5) -> str:
    # ... original implementation

6. Test with a Dedicated Email Sandbox

Before pointing the agent at your real inbox, create a test Gmail account with curated emails. Simulate scenarios: urgent client requests, spam, multi-message threads, calendar invites. Run the agent against this sandbox and validate:

7. Keep the Agent Focused with Scoped Tools

A common failure mode is giving the agent too many tools, which increases the chance of incorrect tool selection. Start with a minimal set (fetch, read, label, mark-read) and only add tools like send_email after proving reliability. Each tool should have a single, clear responsibility and a descriptive docstring that includes usage examples.

Conclusion

Building an email automation agent with LangChain transforms email from a daily burden into a managed, intelligent workflow. By combining the reasoning power of modern LLMs with concrete Gmail API tools, you create a system

— Ad —

Google AdSense will appear here after approval

← Back to all articles