Newsletter Agent

Creating an agent that monitors incoming emails and sends most relevant emails to user

Creating a Newsletter Agent with AgentMail

This is an example implementation of how Agent’s can be equipped with their own inbox and interact with internet resources autonomously, and do the heavy duty lifting of tedious tasks.

Here we will be creating an agent that monitors incoming emails(to its own inbox of course) and sends the most relevant emails regarding newsletter news tailored to the user’s preferences straight to their inbox.

Here is what the agent will be capable of:

  1. Monitor an inbox for new messages
  2. Process newsletter signup requests
  3. Manage promotional content
  4. Send relevant updates to users

Quick Start

This tutorial makes use of the library Browser-use, which requires usage of Python 3.11 or higher. If you want to create an agent in Node please follow the Node.js Tutorial coming soon

1

Install Dependencies

$pip install agentmail asyncio websockets langchain-openai browser-use fastapi python-dotenv httpx

While using the SDK is recommended, if you prefer making direct HTTP requests instead of using the SDK, you’ll only need:

$pip install httpx python-dotenv
3

Set up Environment

Create a .env file in your project root, and add your API key to an LLM. The most common models are 4o with OpenAI or 3.5 sonnet from Anthropic. Feel free to choose one that aligns with your needs but for the sake of this demo we will be using OpenAI

$OPENAI_API_KEY=your_api_key_here
4

Set up AgentMail API Key

Add to your .env file your AgentMail API Key credentials

$AGENTMAIL_API_KEY=your_api_key_here

Creating the Websocket Connection

It seems counterintuitive to have an email sitting in the inbox and just have the agent read the email sitting in the inbox and react to it.

So what we will do is create a websocket connection to the AgentMail inbox, and have it listen for incoming emails. Once the email regarding the coupon code is received, the agent will then be spun up to perform the task outlined.

Lets create a connect file and start by importing the necessary libraries.

connect.py
1import asyncio
2import websockets
3import json
4from agentmail import AgentMail #Import the AgentMail client
5import os
6from dotenv import load_dotenv
7
8load_dotenv()

Lets create a function that will connect to the AgentMail inbox and listen for incoming emails.

connect.py
1async def connect_to_agent(address: str):
2 uri = f"ws://localhost:8000/ws/{address}"
3 client = AgentMail(api_key=os.getenv("AGENTMAIL_API_KEY"))

Websocket is hosted locally, but if you plan on deploying this to production, you will need to host on your own server/domain.

We created the Agentmail client instance, and can interact with the service via SDK calls.

connect.py
1async def connect_to_agent(address: str):
2 uri = f"ws://localhost:8000/ws/{address}"
3 client = AgentMail(api_key=os.getenv("AGENTMAIL_API_KEY"))
4
5 client.inboxes.create(
6 username = address, # Optional, will randomly generate if not provided
7 domain = 'agentmail.to' # Optional, defaults to agentmail.to can use custom domain
8 ) # Our first sdk call!!!
9 print(f"Connected to agent for {email_address}")
10 print("Waiting for new emails...")
11
12 # At this point in the code the inbox is created!
13 async with websockets.connect(uri) as websocket:
14 print(f"Connected to agent for {address}")
15 print("Waiting for new messages...")
16
17 while True:
18 try:
19 # Check for new messages
20 messages = client.messages.list(
21 inbox_id=address, # Required
22 received=True, # Optional: filter for received messages
23 sent=False, # Optional: filter for sent messages
24 limit=10, # Optional: limit number of messages
25 last_key=None # Optional: for pagination
26 )
27
28
29 if messages:
30 notification = {
31 "type": "new_message",
32 "data": {
33 "address": address,
34 "messages": [{"id": msg.id, "subject": msg.subject} for msg in messages]
35 }
36 }
37 await websocket.send(json.dumps(notification))
38
39 response = await websocket.recv()
40 response_data = json.loads(response)
41
42 if response_data.get('status') == 'success':
43 print("Message processing completed")
44 return
45
46 await asyncio.sleep(5) # Check every 5 seconds
47
48 except websockets.ConnectionClosed:
49 print("Connection closed")
50 break
51 except Exception as e:
52 print(f"Error: {e}")
53 await asyncio.sleep(5)

If you prefer making direct HTTP requests instead of using the SDK:

connect.py
1response = httpx.get(
2 f"https://api.agentmail.to/v0/inboxes/{inbox_address}/messages",
3 params={
4 "received": True,
5 "sent": False,
6 "limit": 10,
7 "last_key": None
8 },
9 headers={"Authorization": f"Bearer {os.getenv('AGENTMAIL_API_KEY')}"}
10)
11messages = response.json()

Make sure to handle rate limits appropriately. The 5-second interval might need adjustment based on your use case and API limits.

What this code does is it creates a websocket connection to the AgentMail inbox and constatly checks for new emails in 5 second intervals via the get_emails() function.

Once the email is received, it sends a notification through the websocket to our own server to spin the agent up to perform the task outlined.

Creating the Agent

Lets get the imports out of the way first.

agent.py
1from langchain_openai import ChatOpenAI
2from browser_use import Agent, BrowserConfig, Browser
3from browser_use.browser.context import BrowserContextConfig
4from playwright.async_api import BrowserContext
5from dotenv import load_dotenv
6from browser_use.controller.service import Controller
7from agentmail import AgentMail
8from typing import Optional, List
9from fastapi import FastAPI, WebSocket
10from fastapi.middleware.cors import CORSMiddleware

Browser

We will be using the Browser-use library to create a browser instance and give our agent access to our browser so it can browse the web and sign up for the coupon codes.

The implementation of the browser-use library is a bit complex, but the basic idea is that we are creating a browser instance and then passing it to the agent and for the sake of this demo we will give it the following configurations:

  1. Headless mode is set to false, so the browser will be visible to the user.
  2. Disable security is set to true, so the browser will not be secure.
  3. Chrome instance path is set to the path of the chrome browser on your machine.
  4. New context config is set to the following:

You can play around with the configurations and see what works best for you. Here is the browser-use documentation for more information.

Lets load the .env file and create the browser instance.

agent.py
1load_dotenv()
2
3browser = Browser(
4 config=BrowserConfig(
5 headless=False,
6 disable_security=True,
7 chrome_instance_path="/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
8 new_context_config=BrowserContextConfig(
9 wait_for_network_idle_page_load_time=3.0,
10 browser_window_size={'width': 1280, 'height': 1100},
11 )
12 )
13)

Lets create the instance that will manage the email inbox and websocket connection.

agent.py
1llm = ChatOpenAI(model="gpt-4o")
2
3class EmailManager:
4 def __init__(self):
5 self.active_connections: Dict[str, WebSocket] = {}
6 self.agents: Dict[str, Agent] = {}
7
8 async def connect(self, email_address: str, websocket: WebSocket):
9 await websocket.accept()
10 self.active_connections[email_address] = websocket
11
12 # Clear any existing log file
13 log_path = f"logs/conversation_{email_address}.json"
14 if os.path.exists(log_path):
15 os.remove(log_path)
16
17 # Create agent instance for this connection
18 self.agents[email_address] = Agent(
19 task=task,
20 llm=llm,
21 use_vision=True,
22 save_conversation_path=f"logs/conversation_{email_address}.json",
23 browser=browser,
24 controller=controller
25 )
26
27 async def disconnect(self, email_address: str):
28 if email_address in self.active_connections:
29 del self.active_connections[email_address]
30 if email_address in self.agents:
31 del self.agents[email_address]
32
33 async def process_email_update(self, email_address: str, data: dict):
34 if email_address in self.agents:
35 agent = self.agents[email_address]
36 await agent.run()
You’ll notice that we cleared any existing log file before creating a new agent instance. This is because the agent has a conversation log that is saved to a file. If you don’t clear the file, the agent will continue to read the same conversation log, and may refuse to the task if it already succesfully completed it on a previous run.

The task is an f string that will be passed to the agent as instructions. Here is the task that we will be passing:

agent.py
1task = f"""
2You are Agent Kelly, an email and web assistant. Your personal email url is {email_address}
3 Do not attempt to create your own inbox. You already have one.
4 You must follow these strict rules:
5 1. ALWAYS use the provided tool calls for ANY email operations - never try to access emails directly via URLs
6 2. Use 'Get all messages' tool to check for new emails
7 3. When you receive messages, first get their IDs from the messages.messages list returned by 'Get all messages'
8 4. Loop through each message and check message.subject and if the word Newsletter is in the subject use this email address to get the message content
9 - save this original message.from field - this is who you'll reply to later
10 - save this original message_id for replying later
11 5. When you find the Newsletter Signup Request message, read it by calling 'Get message content' with the message ID.
12 6. DO NOT go to the substack website. Directly search the newsletter name + 'signup' in google and sign up for the top link
13 7. After signing up, there will be a second screen asking you to sign up to donate. Select the fourth option which means none. Then just click through skipping and then finally maybe later. Don't close the tab until you see the confirmation that you are subscribed.'
14 8. Be sure to not click the check box to confirm before clicking submit since it is already checked.
15 9. Use 'reply_to_message' tool to reply to the newsletter signup request message saying you succesfully signed up and will keep the user updated with everything they requested to be kept to up to date with. Use the original message ID you used earlier. For new lines, don't use double slash n. Use a single slash n. In the message, say that you are subscribed to the newsletter, and will be forwarding the best deals that align with the user's interests in the future.
16 - Use ONLY inbox_id and message_id parameters from the newsletter signup request when replying
17 - make sure to reply to the original sender
18 - DO NOT reply to the welcome email or your own inbox.
19 - Include a confirmation message about the subscription
20 - use the newsletter signup request messages message.from as the to address here. It should never be your own. And you shouldn't you assume and create one.
21 Your goal is to:
22 1. Monitor your inbox for a Newsletter Signup Request email
23 2. When found, sign up for the requested fashion brand newsletters
24 3. Reply with a kind email using the reply_to_message tool with the original message ID. and titled 'Best Promotions' including a summary of the newsletter on LLMs.
25
26 Remember: You must ONLY interact with emails through the provided tools.
27 Example workflow:
28 1. messages = Get all messages
29 2. if messages.messages exists and has items:
30 - message_id = messages.messages[0].message_id
31 - Use 'Get specific message' with message_id to read it
32
33 Remember: Never call 'Get all messages' repeatedly without processing the results.
34 """

Thats quite lengthy, but it ensures accuracy and the agent will not deviate from the task.

With that out of the way, we will create the following:

  1. Browser Instance (to browse the web)
  2. Controller Instance (browser-use way of defining tools to give to the agent)
  3. App Instance (to host the websocket connection)
  4. AgenetMail Instance (to interact with the inbox)
agent.py
1email_manager = EmailManager()
2
3browser = Browser()
4controller = Controller()
5client = AgentMail(api_key=os.getenv("AGENTMAIL_API_KEY"))
6app = FastAPI()
7app.add_middleware(
8 CORSMiddleware,
9 allow_origins=["*"],
10 allow_credentials=True,
11 allow_methods=["*"],
12 allow_headers=["*"],
13)

Tool Calls

Here is the fun part. Tool calls.

We will be creating tool calls for all the necessary endpoints, but feel free to play around with the code and create your own.

Browser-use has a specific way to define custom functions(tools) that will be passed to the agent. This is in the form of controller.action(). It effectively is the @tool decorator in langchain.

Make sure to add succinct but descriptive docstrings if you are creating complex tools.
agent.py
1@controller.action('Create new inbox')
2def create_inbox() -> str:
3 result = client.inboxes.create()
4 return f"Created inbox: {result.address}"
5
6@controller.action('Delete inbox')
7def delete_inbox(inbox_id: str) -> str:
8 client.inboxes.delete(inbox_id=inbox_id)
9 return f"Deleted inbox: {inbox_id}"
10
11@controller.action('Get all messages from inbox')
12def get_messages(inbox_id: str) -> str:
13 messages = client.messages.list(
14 inbox_id=inbox_id,
15
16 )
17 if not messages.messages:
18 return "No messages found in inbox"
19
20 message_ids = []
21 for msg in messages.messages:
22 message_ids.append(msg.message_id)
23 return f"Retrieved {len(message_ids)} messages from {inbox_id} with IDs: {message_ids}"
24
25@controller.action('Get message by ID')
26def get_message(inbox_id: str, message_id: str) -> str:
27 message = client.messages.get(inbox_id = inbox_id, message_id=message_id)
28 return f"Retrieved message: {message.subject}"
29
30@controller.action('Get message content')
31def get_message(inbox_id: str, message_id: str) -> str:
32 message = client.messages.get(inbox_id = inbox_id, message_id=message_id)
33 return f"Retrieved message: {message.text}"
34
35@controller.action('Send new message')
36def send_message(
37 inbox_id: str,
38 to: List[str],
39 subject: Optional[str] = None,
40 text: Optional[str] = None,
41 html: Optional[str] = None,
42 cc: Optional[List[str]] = None,
43 bcc: Optional[List[str]] = None
44):
45
46 client.messages.send(
47 inbox_id=inbox_id,
48 to=to,
49 subject=subject or "",
50 text=text or "",
51 html=html or "",
52 cc=cc or [],
53 bcc=bcc or []
54 )
55 return f"Sent message from {inbox_id} to {to}"
56
57@controller.action('Reply to message')
58def reply_to_message(
59 inbox_id: str,
60 message_id: str,
61 text: Optional[str] = None,
62 html: Optional[str] = None,
63 to: Optional[str] = None,
64 cc: Optional[List[str]] = None,
65 bcc: Optional[List[str]] = None
66):
67 client.messages.reply(
68 inbox_id=inbox_id,
69 message_id=message_id,
70 text=text or "",
71 html=html or "",
72 to=to or "",
73 cc=cc or [],
74 bcc=bcc or [],
75 )
76 return f"Replied to message {message_id}"

These are some elementary tool calls we defined ourselves. Please feel free to play around with the code and create your own given the methods available in the AgentMail SDK.

Finally, lets define the websocket endpoint that will be used to receive the notification a new email just came in, so we can spin up the agent to perform the task outlined.

agent.py
1@app.websocket("/ws/{email_address}")
2async def websocket_endpoint(websocket: WebSocket, email_address: str):
3 await email_manager.connect(email_address, websocket)
4 try:
5 while True:
6 print("Waiting for WebSocket message...")
7 data = await websocket.receive_json()
8 print(f"Received data: {data}")
9
10 if data.get('type') == 'new_email':
11 await email_manager.process_email_update(email_address, data)
12 await websocket.send_json({
13 "status": "success",
14 "message": "Email processed successfully"
15 })
16 print("Processing complete, closing connection")
17 await websocket.close()
18 break # Exit the loop
19 except Exception as e:
20 print(f"Error in websocket connection: {e}")
21 finally:
22 await email_manager.disconnect(email_address)

We communicate via these json messages, but feel free to play around with what information is sent over the websocket.

Finally, lets run the app:

agent.py
1if __name__ == "__main__":
2 import uvicorn
3 uvicorn.run(app, host="0.0.0.0", port=8000)

BOOM!

You’re done. Open up 2 terminals and run the following:

$python agent.py
>python connect.py

Running agent.py will open the websocket connection via uvicorn and fast API. Connect.py will connect to this websocket connection and listen for new emails.

Once it receives one it will send a notification to the agent.py file, which will spin up the agent and perform the task outlined.

To see everything happen in real time, open up the browser and go to the inbox of the email you are monitoring on the AgentMail demo site

Feel free to play around with the code and see what you can come up with!

You can find here the full code for the Shopper Agent

Happy coding!