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
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions finbot/apps/admin/routes/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -335,6 +335,9 @@ async def send_message(
bcc=req.bcc,
)

if result.get("error"):
raise HTTPException(status_code=400, detail=result["error"])

external = [d for d in result.get("deliveries", []) if d["type"] == "external"]
if external:
from finbot.core.messaging import event_bus # pylint: disable=import-outside-toplevel
Expand Down
3 changes: 3 additions & 0 deletions finbot/apps/vendor/routes/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -1136,6 +1136,9 @@ async def send_message(
bcc=req.bcc,
)

if result.get("error"):
raise HTTPException(status_code=400, detail=result["error"])

external = [d for d in result.get("deliveries", []) if d["type"] == "external"]
if external:
await event_bus.emit_business_event(
Expand Down
22 changes: 22 additions & 0 deletions finbot/mcp/servers/finmail/routing.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@

logger = logging.getLogger(__name__)

MAX_EMAIL_ADDRESS_LENGTH = 254


def get_admin_address(namespace: str) -> str:
"""Derive the canonical admin address from a namespace."""
Expand Down Expand Up @@ -48,6 +50,22 @@ def _is_internal_address(email_addr: str, namespace: str) -> bool:
return email_addr.lower().endswith(f"@{namespace.lower()}.finbot")


def _normalize_and_validate_email_address(email_addr: str) -> tuple[str | None, str | None]:
"""Normalize a recipient address and reject obviously invalid values."""
normalized = email_addr.strip() if isinstance(email_addr, str) else ""

if not normalized:
return None, "Email address is required"

if len(normalized) > MAX_EMAIL_ADDRESS_LENGTH:
return (
None,
f"Email address exceeds maximum length of {MAX_EMAIL_ADDRESS_LENGTH} characters",
)

return normalized, None


def route_and_deliver(
db: Session,
repo: EmailRepository,
Expand Down Expand Up @@ -75,6 +93,10 @@ def route_and_deliver(

for role, addresses in [("to", to), ("cc", cc), ("bcc", bcc)]:
for email_addr in (addresses or []):
email_addr, validation_error = _normalize_and_validate_email_address(email_addr)
if validation_error:
return {"error": validation_error}

visible_bcc = bcc_json if role == "bcc" else None

vendor = (
Expand Down
25 changes: 22 additions & 3 deletions finbot/static/js/admin/messages.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ const TYPE_ICONS = {
reminder: '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"/>',
};

const MAX_EMAIL_ADDRESS_LENGTH = 254;

ready(function () {
initializeInbox();
});
Expand Down Expand Up @@ -377,22 +379,38 @@ function parseAddresses(value) {
return value.split(',').map(s => s.trim()).filter(Boolean);
}

function validateAddresses(addresses) {
if (!addresses) return null;

const invalid = addresses.find(addr => addr.length > MAX_EMAIL_ADDRESS_LENGTH);
if (invalid) {
return `Each email address must be ${MAX_EMAIL_ADDRESS_LENGTH} characters or fewer`;
}

return null;
}

async function sendComposedEmail() {
const to = parseAddresses(document.getElementById('compose-to')?.value);
const cc = parseAddresses(document.getElementById('compose-cc')?.value);
const bcc = parseAddresses(document.getElementById('compose-bcc')?.value);
const subject = document.getElementById('compose-subject')?.value?.trim();
const body = document.getElementById('compose-body')?.value?.trim();

if (!to || to.length === 0) return showNotification('To address is required', 'error');
if (!subject) return showNotification('Subject is required', 'error');
if (!body) return showNotification('Message body is required', 'error');

const addressError = validateAddresses([...(to || []), ...(cc || []), ...(bcc || [])]);
if (addressError) return showNotification(addressError, 'error');

const payload = {
to,
subject,
body,
message_type: 'general',
cc: parseAddresses(document.getElementById('compose-cc')?.value),
bcc: parseAddresses(document.getElementById('compose-bcc')?.value),
cc,
bcc,
};

try {
Expand All @@ -403,7 +421,8 @@ async function sendComposedEmail() {
await loadMessages();
} catch (err) {
console.error('Failed to send email:', err);
showNotification('Failed to send email', 'error');
const message = err?.response?.data?.detail || 'Failed to send email';
showNotification(message, 'error');
}
}

Expand Down
14 changes: 12 additions & 2 deletions finbot/static/js/common/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -73,8 +73,13 @@ class FinBotAPI {
}

if (!response.ok) {
const errorMessage =
data?.detail ||
data?.message ||
data?.error?.message ||
`HTTP ${response.status}: ${response.statusText}`;
throw new APIError(
data.message || `HTTP ${response.status}: ${response.statusText}`,
errorMessage,
response.status,
data
);
Expand Down Expand Up @@ -284,7 +289,12 @@ function handleAPIError(error, options = {}) {
}

// Show user-friendly error message
const message = error.data?.message || error.message || 'An error occurred';
const message =
error.data?.detail ||
error.data?.message ||
error.data?.error?.message ||
error.message ||
'An error occurred';

if (options.showAlert !== false) {
showNotification(message, 'danger');
Expand Down
25 changes: 22 additions & 3 deletions finbot/static/js/vendor/messages.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ const TYPE_ICONS = {
reminder: '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"/>',
};

const MAX_EMAIL_ADDRESS_LENGTH = 254;

ready(function () {
initializeInbox();
});
Expand Down Expand Up @@ -385,22 +387,38 @@ function parseAddresses(value) {
return value.split(',').map(s => s.trim()).filter(Boolean);
}

function validateAddresses(addresses) {
if (!addresses) return null;

const invalid = addresses.find(addr => addr.length > MAX_EMAIL_ADDRESS_LENGTH);
if (invalid) {
return `Each email address must be ${MAX_EMAIL_ADDRESS_LENGTH} characters or fewer`;
}

return null;
}

async function sendComposedEmail() {
const to = parseAddresses(document.getElementById('compose-to')?.value);
const cc = parseAddresses(document.getElementById('compose-cc')?.value);
const bcc = parseAddresses(document.getElementById('compose-bcc')?.value);
const subject = document.getElementById('compose-subject')?.value?.trim();
const body = document.getElementById('compose-body')?.value?.trim();

if (!to || to.length === 0) return showNotification('To address is required', 'error');
if (!subject) return showNotification('Subject is required', 'error');
if (!body) return showNotification('Message body is required', 'error');

const addressError = validateAddresses([...(to || []), ...(cc || []), ...(bcc || [])]);
if (addressError) return showNotification(addressError, 'error');

const payload = {
to,
subject,
body,
message_type: 'general',
cc: parseAddresses(document.getElementById('compose-cc')?.value),
bcc: parseAddresses(document.getElementById('compose-bcc')?.value),
cc,
bcc,
};

try {
Expand All @@ -411,7 +429,8 @@ async function sendComposedEmail() {
await loadMessages();
} catch (err) {
console.error('Failed to send email:', err);
showNotification('Failed to send email', 'error');
const message = err?.response?.data?.detail || 'Failed to send email';
showNotification(message, 'error');
}
}

Expand Down
Loading