Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
The diff you're trying to view is too large. We only load the first 3000 changed files.
Binary file added app/__pycache__/__init__.cpython-312.pyc
Binary file not shown.
Binary file added app/__pycache__/main.cpython-312.pyc
Binary file not shown.
Binary file added app/core/__pycache__/__init__.cpython-312.pyc
Binary file not shown.
Binary file added app/core/__pycache__/config.cpython-312.pyc
Binary file not shown.
Binary file added app/db/__pycache__/__init__.cpython-312.pyc
Binary file not shown.
Binary file added app/db/__pycache__/database.cpython-312.pyc
Binary file not shown.
32 changes: 25 additions & 7 deletions app/main.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from fastapi import FastAPI, Depends, HTTPException, status
from fastapi import FastAPI, Depends, HTTPException, status, Form
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from sqlalchemy.orm import Session
from typing import List
Expand All @@ -13,10 +13,28 @@
# Create tables
Base.metadata.create_all(bind=engine)

import os
from fastapi.responses import FileResponse
from fastapi.staticfiles import StaticFiles

app = FastAPI(title="Agent Suite", version="0.1.0")
security = HTTPBearer()
settings = get_settings()

BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
STATIC_DIR = os.path.join(BASE_DIR, "static")

os.makedirs(STATIC_DIR, exist_ok=True)
app.mount("/static", StaticFiles(directory=STATIC_DIR), name="static")

@app.get("/inbox")
def get_inbox_page():
return FileResponse(os.path.join(STATIC_DIR, "inbox.html"))

@app.get("/compose")
def get_compose_page():
return FileResponse(os.path.join(STATIC_DIR, "compose.html"))


def get_inbox_by_api_key(api_key: str, db: Session):
return db.query(models.Inbox).filter(
Expand Down Expand Up @@ -143,12 +161,12 @@ def list_messages(

@app.post("/v1/webhooks/mailgun")
def mailgun_webhook(
sender: str,
recipient: str,
subject: str = "",
body_plain: str = "",
body_html: str = "",
message_id: str = "",
sender: str = Form(...),
recipient: str = Form(...),
subject: str = Form(""),
body_plain: str = Form(""),
body_html: str = Form(""),
message_id: str = Form(""),
db: Session = Depends(get_db)
):
"""Receive incoming email from Mailgun."""
Expand Down
Binary file added app/models/__pycache__/__init__.cpython-312.pyc
Binary file not shown.
Binary file added app/models/__pycache__/models.cpython-312.pyc
Binary file not shown.
9 changes: 4 additions & 5 deletions app/models/models.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import uuid
from datetime import datetime
from sqlalchemy import Column, String, DateTime, Text, ForeignKey, Boolean
from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy import Column, String, DateTime, Text, ForeignKey, Boolean, Uuid
from app.db.database import Base


Expand All @@ -12,7 +11,7 @@ def generate_api_key():
class Inbox(Base):
__tablename__ = "inboxes"

id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
id = Column(Uuid(as_uuid=True), primary_key=True, default=uuid.uuid4)
email_address = Column(String(255), unique=True, index=True, nullable=False)
api_key = Column(String(255), unique=True, index=True, default=generate_api_key)
created_at = Column(DateTime, default=datetime.utcnow)
Expand All @@ -22,8 +21,8 @@ class Inbox(Base):
class Message(Base):
__tablename__ = "messages"

id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
inbox_id = Column(UUID(as_uuid=True), ForeignKey("inboxes.id"), index=True)
id = Column(Uuid(as_uuid=True), primary_key=True, default=uuid.uuid4)
inbox_id = Column(Uuid(as_uuid=True), ForeignKey("inboxes.id"), index=True)
sender = Column(String(255), nullable=False)
recipient = Column(String(255), nullable=False)
subject = Column(String(500))
Expand Down
Binary file added app/schemas/__pycache__/__init__.cpython-312.pyc
Binary file not shown.
Binary file added app/schemas/__pycache__/schemas.cpython-312.pyc
Binary file not shown.
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@ boto3==1.34.0
python-multipart==0.0.6
pytest==7.4.4
httpx==0.26.0
email-validator==2.2.0
122 changes: 122 additions & 0 deletions static/compose.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Compose - Agent Suite</title>
<style>
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background-color: #121212; color: #e0e0e0; margin: 0; padding: 20px; }
.container { max-width: 800px; margin: 0 auto; }
header { display: flex; justify-content: space-between; align-items: center; border-bottom: 1px solid #333; padding-bottom: 10px; margin-bottom: 20px; }
a { color: #bb86fc; text-decoration: none; }
.api-key-section { margin-bottom: 20px; padding: 15px; background: #1e1e1e; border-radius: 8px; }
.form-group { margin-bottom: 15px; }
label { display: block; margin-bottom: 5px; font-weight: bold; }
input[type="text"], input[type="email"], textarea { width: 100%; padding: 10px; background: #2c2c2c; border: 1px solid #444; color: white; border-radius: 4px; box-sizing: border-box; }
textarea { height: 200px; resize: vertical; }
button { background: #bb86fc; color: #000; border: none; padding: 10px 15px; cursor: pointer; border-radius: 4px; font-weight: bold; }
button:hover { background: #9965f4; }
button:disabled { background: #555; cursor: not-allowed; }
.error { color: #cf6679; margin-top: 10px; }
.success { color: #03dac6; margin-top: 10px; }
</style>
</head>
<body>
<div class="container">
<header>
<h1>Agent Suite - Compose</h1>
<nav>
<a href="/inbox">Inbox</a> | <a href="/compose">Compose</a>
</nav>
</header>

<div class="api-key-section">
<label for="apiKey">API Key:</label>
<input type="text" id="apiKey" placeholder="Enter your API key">
<button onclick="saveApiKey()">Save</button>
</div>

<form id="composeForm">
<div class="form-group">
<label for="to">To:</label>
<input type="email" id="to" required placeholder="recipient@example.com">
</div>
<div class="form-group">
<label for="subject">Subject:</label>
<input type="text" id="subject" required placeholder="Email subject">
</div>
<div class="form-group">
<label for="body">Message:</label>
<textarea id="body" required placeholder="Write your message here..."></textarea>
</div>
<button type="submit" id="sendBtn">Send Email</button>
<div id="statusMsg"></div>
</form>
</div>

<script>
const apiKeyInput = document.getElementById('apiKey');
const composeForm = document.getElementById('composeForm');
const sendBtn = document.getElementById('sendBtn');
const statusMsg = document.getElementById('statusMsg');

// Load API key from local storage
if (localStorage.getItem('agentSuiteApiKey')) {
apiKeyInput.value = localStorage.getItem('agentSuiteApiKey');
}

function saveApiKey() {
localStorage.setItem('agentSuiteApiKey', apiKeyInput.value);
statusMsg.className = 'success';
statusMsg.innerText = 'API key saved.';
setTimeout(() => statusMsg.innerText = '', 2000);
}

composeForm.addEventListener('submit', async (e) => {
e.preventDefault();
const apiKey = apiKeyInput.value;

if (!apiKey) {
statusMsg.className = 'error';
statusMsg.innerText = 'Please save your API key first.';
return;
}

const to = document.getElementById('to').value;
const subject = document.getElementById('subject').value;
const body = document.getElementById('body').value;

sendBtn.disabled = true;
statusMsg.className = '';
statusMsg.innerText = 'Sending...';

try {
const response = await fetch('/v1/inboxes/me/send', {
method: 'POST',
headers: {
'Authorization': `Bearer ${apiKey}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({ to, subject, body })
});

if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.detail || 'Failed to send email');
}

statusMsg.className = 'success';
statusMsg.innerText = 'Email sent successfully!';
document.getElementById('to').value = '';
document.getElementById('subject').value = '';
document.getElementById('body').value = '';
} catch (err) {
statusMsg.className = 'error';
statusMsg.innerText = err.message;
} finally {
sendBtn.disabled = false;
}
});
</script>
</body>
</html>
119 changes: 119 additions & 0 deletions static/inbox.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Inbox - Agent Suite</title>
<style>
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background-color: #121212; color: #e0e0e0; margin: 0; padding: 20px; }
.container { max-width: 800px; margin: 0 auto; }
header { display: flex; justify-content: space-between; align-items: center; border-bottom: 1px solid #333; padding-bottom: 10px; margin-bottom: 20px; }
a { color: #bb86fc; text-decoration: none; }
.api-key-section { margin-bottom: 20px; padding: 15px; background: #1e1e1e; border-radius: 8px; }
input[type="text"], input[type="email"], textarea { width: 100%; padding: 10px; margin: 5px 0; background: #2c2c2c; border: 1px solid #444; color: white; border-radius: 4px; box-sizing: border-box; }
button { background: #bb86fc; color: #000; border: none; padding: 10px 15px; cursor: pointer; border-radius: 4px; font-weight: bold; }
button:hover { background: #9965f4; }
.message-list { list-style: none; padding: 0; }
.message-item { background: #1e1e1e; margin-bottom: 10px; padding: 15px; border-radius: 8px; cursor: pointer; border: 1px solid #333; }
.message-item:hover { border-color: #bb86fc; }
.message-header { display: flex; justify-content: space-between; margin-bottom: 5px; }
.message-subject { font-weight: bold; font-size: 1.1em; }
.message-sender { color: #aaa; font-size: 0.9em; }
.message-content { display: none; margin-top: 15px; padding-top: 15px; border-top: 1px solid #333; white-space: pre-wrap; color: #ccc; }
.error { color: #cf6679; margin-top: 10px; }
</style>
</head>
<body>
<div class="container">
<header>
<h1>Agent Suite - Inbox</h1>
<nav>
<a href="/inbox">Inbox</a> | <a href="/compose">Compose</a>
</nav>
</header>

<div class="api-key-section">
<label for="apiKey">API Key:</label>
<input type="text" id="apiKey" placeholder="Enter your API key">
<button onclick="saveApiKey()">Save</button>
<div id="errorMsg" class="error"></div>
</div>

<ul id="messagesList" class="message-list">
<li>Loading messages...</li>
</ul>
</div>

<script>
const apiKeyInput = document.getElementById('apiKey');
const messagesList = document.getElementById('messagesList');
const errorMsg = document.getElementById('errorMsg');

// Load API key from local storage
if (localStorage.getItem('agentSuiteApiKey')) {
apiKeyInput.value = localStorage.getItem('agentSuiteApiKey');
fetchMessages();
}

function saveApiKey() {
localStorage.setItem('agentSuiteApiKey', apiKeyInput.value);
fetchMessages();
}

async function fetchMessages() {
const apiKey = apiKeyInput.value;
if (!apiKey) return;

errorMsg.innerText = '';
messagesList.innerHTML = '<li>Loading messages...</li>';

try {
const response = await fetch('/v1/inboxes/me/messages', {
headers: { 'Authorization': `Bearer ${apiKey}` }
});

if (!response.ok) {
throw new Error(response.status === 401 ? 'Invalid API Key' : 'Failed to load messages');
}

const data = await response.json();
renderMessages(data.messages);
} catch (err) {
errorMsg.innerText = err.message;
messagesList.innerHTML = '';
}
}

function renderMessages(messages) {
if (!messages || messages.length === 0) {
messagesList.innerHTML = '<li>No messages found.</li>';
return;
}

messagesList.innerHTML = '';
messages.forEach(msg => {
const li = document.createElement('li');
li.className = 'message-item';

const date = new Date(msg.received_at).toLocaleString();

li.innerHTML = `
<div class="message-header">
<span class="message-sender">${msg.sender}</span>
<span class="message-date" style="color: #888; font-size: 0.8em;">${date}</span>
</div>
<div class="message-subject">${msg.subject || '(No Subject)'}</div>
<div class="message-content">${msg.body_text || msg.body_html || '(No Content)'}</div>
`;

li.addEventListener('click', () => {
const content = li.querySelector('.message-content');
content.style.display = content.style.display === 'block' ? 'none' : 'block';
});

messagesList.appendChild(li);
});
}
</script>
</body>
</html>
Binary file added test.db
Binary file not shown.
Binary file not shown.
Binary file not shown.
4 changes: 4 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import os

# Force tests onto sqlite before app modules import settings/engine.
os.environ.setdefault("DATABASE_URL", "sqlite:///./test.db")
13 changes: 13 additions & 0 deletions tests/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,3 +97,16 @@ def test_list_messages(setup_db):
data = response.json()
assert data["total"] == 1
assert data["messages"][0]["subject"] == "Test Subject"


def test_inbox_page_served():
response = client.get("/inbox")
assert response.status_code == 200
assert "Agent Suite - Inbox" in response.text



def test_compose_page_served():
response = client.get("/compose")
assert response.status_code == 200
assert "Agent Suite - Compose" in response.text
Loading