pull/1/head
ahmed.mujtaba 2026-08-03 20:35:47 +05:00
parent a9f0b32eea
commit 04a6264bb0
5 changed files with 192 additions and 23 deletions

View File

@ -26,10 +26,30 @@ async def fetch_email(top:int=Query(100),skip:int=Query(0,ge=0),token=Query(...)
return JSONResponse(content={"data":items_lst,"status_code":200}) return JSONResponse(content={"data":items_lst,"status_code":200})
except HTTPException:
raise
except Exception as e: except Exception as e:
raise HTTPException(status_code=500,detail=str(e)) raise HTTPException(status_code=500,detail=str(e))
@router.post("/inbox/fetch") @router.get("/inbox/fetch")
async def fetch_inbox(session: AsyncSession = Depends(get_session)): async def fetch_inbox(
pass record_id: str | None = Query(None),
search: str | None = Query(None),
top: int | None = Query(None),
skip: int = Query(0, ge=0),
session: AsyncSession = Depends(get_session),
):
try:
service=Email(session=session)
if record_id:
item=await service.get_inbox_message_by_id(record_id)
return JSONResponse(content={"data":item,"total":1,"status_code":200})
items=await service.get_inbox_messages(top,skip,search)
total=await service.count_inbox_messages(search)
return JSONResponse(content={"data":items,"total":total,"status_code":200})
except HTTPException:
raise
except Exception as e:
raise HTTPException(status_code=500,detail=str(e))

View File

@ -2,10 +2,10 @@ import uuid
from datetime import datetime from datetime import datetime
from typing import Any, Optional from typing import Any, Optional
from sqlalchemy import Column from sqlalchemy import Column, func, or_
from sqlalchemy.dialects.postgresql import JSONB from sqlalchemy.dialects.postgresql import JSONB
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from sqlmodel import Field, Relationship, SQLModel from sqlmodel import Field, Relationship, SQLModel, select
from users.models import Users from users.models import Users
@ -45,6 +45,7 @@ class Inbox_Messages(SQLModel, table=True):
__tablename__ = "inbox_messages" __tablename__ = "inbox_messages"
id: uuid.UUID = Field(default_factory=uuid.uuid4, primary_key=True) id: uuid.UUID = Field(default_factory=uuid.uuid4, primary_key=True)
message_id: str | None = Field(default=None, index=True, unique=True)
full_email_response: dict[str, Any] | None = Field( full_email_response: dict[str, Any] | None = Field(
default=None, sa_column=Column(JSONB) default=None, sa_column=Column(JSONB)
) )
@ -73,31 +74,93 @@ class Inbox_Messages(SQLModel, table=True):
return email_data.get("bodyPreview") or "" return email_data.get("bodyPreview") or ""
@classmethod @classmethod
async def insert_email(cls, session: AsyncSession, email_data: dict): def _fields_from_email(cls, email_data: dict) -> dict:
email = cls( return {
message_subject=email_data.get("subject") or "", "message_subject": email_data.get("subject") or "",
message_body=cls._body_text(email_data), "message_body": cls._body_text(email_data),
message_sent_time=email_data.get("sentDateTime") or "", "message_sent_time": email_data.get("sentDateTime") or "",
message_read=bool(email_data.get("isRead")), "message_read": bool(email_data.get("isRead")),
message_received_time=email_data.get("receivedDateTime") or "", "message_received_time": email_data.get("receivedDateTime") or "",
message_from=email_data.get("from", {}) "message_from": email_data.get("from", {})
.get("emailAddress", {}) .get("emailAddress", {})
.get("address", ""), .get("address", ""),
message_to=",".join( "message_to": ",".join(
[r["emailAddress"]["address"] for r in email_data.get("toRecipients", [])] [r["emailAddress"]["address"] for r in email_data.get("toRecipients", [])]
), ),
message_cc=",".join( "message_cc": ",".join(
[r["emailAddress"]["address"] for r in email_data.get("ccRecipients", [])] [r["emailAddress"]["address"] for r in email_data.get("ccRecipients", [])]
), ),
message_bcc=",".join( "message_bcc": ",".join(
[r["emailAddress"]["address"] for r in email_data.get("bccRecipients", [])] [r["emailAddress"]["address"] for r in email_data.get("bccRecipients", [])]
), ),
attachment=bool(email_data.get("hasAttachments")), "attachment": bool(email_data.get("hasAttachments")),
message_reply=",".join( "message_reply": ",".join(
[r["emailAddress"]["address"] for r in email_data.get("replyTo", [])] [r["emailAddress"]["address"] for r in email_data.get("replyTo", [])]
), ),
full_email_response=email_data, "message_id": email_data.get("id"),
) "full_email_response": email_data,
}
@classmethod
async def insert_email(cls, session: AsyncSession, email_data: dict):
fields = cls._fields_from_email(email_data)
external_id = fields.get("message_id")
if external_id:
existing = (
await session.execute(
select(cls).where(cls.message_id == external_id)
)
).scalars().first()
if existing:
for key, value in fields.items():
setattr(existing, key, value)
session.add(existing)
await session.commit()
await session.refresh(existing)
return existing
email = cls(**fields)
session.add(email) session.add(email)
await session.commit() await session.commit()
return email return email
@classmethod
def _search_filter(cls, search: str):
pattern = f"%{search}%"
return or_(
cls.message_subject.ilike(pattern),
cls.message_from.ilike(pattern),
cls.message_body.ilike(pattern),
)
@classmethod
async def get_inbox_messages(
cls, session: AsyncSession, top: int | None, skip: int, search: str | None
):
statement = select(cls).order_by(cls.message_received_time.desc())
if search:
statement = statement.where(cls._search_filter(search))
if skip:
statement = statement.offset(skip)
if top is not None:
statement = statement.limit(top)
result = await session.execute(statement)
return result.scalars().all()
@classmethod
async def get_inbox_message_by_id(cls, session: AsyncSession, record_id: str):
try:
uid = uuid.UUID(str(record_id))
except ValueError:
return None
result = await session.execute(select(cls).where(cls.id == uid))
return result.scalars().first()
@classmethod
async def count_inbox_messages(cls, session: AsyncSession, search: str | None):
statement = select(func.count()).select_from(cls)
if search:
statement = statement.where(cls._search_filter(search))
result = await session.execute(statement)
return result.scalar_one()

View File

@ -0,0 +1,40 @@
from pathlib import Path
from inbox.models import Inbox_Messages
def serialize_message(message: Inbox_Messages) -> dict:
"""inbox_messages row -> the shape the #inbox Email tab renders."""
sender_name = message.message_from
full = message.full_email_response
if isinstance(full, dict):
from_block = full.get("from")
if isinstance(from_block, dict):
email_address = from_block.get("emailAddress")
if isinstance(email_address, dict):
name = email_address.get("name")
if name:
sender_name = name
attachment_name = None
if message.file_path:
attachment_name = Path(message.file_path).name
return {
"id": str(message.id),
"message_id": str(message.message_id) if message.message_id else None,
"sender_name": sender_name,
"fromEmail": message.message_from,
"subject": message.message_subject,
"body": message.message_body,
"when": message.message_received_time,
"unread": not message.message_read,
"attachment": message.attachment,
"attachment_name": attachment_name,
"message_to": message.message_to,
"message_cc": message.message_cc,
"message_bcc": message.message_bcc,
"message_sent_time": message.message_sent_time,
"message_reply": message.message_reply,
"file_path": message.file_path,
}

View File

@ -1,14 +1,17 @@
import httpx,os import httpx,os,uuid
from fastapi import HTTPException from fastapi import HTTPException
from inbox.models import Inbox_Messages from inbox.models import Inbox_Messages
from inbox.file_decoder import decode_attachment, AttachmentDecodeError from inbox.file_decoder import decode_attachment, AttachmentDecodeError
from inbox.serializers import serialize_message
from dotenv import load_dotenv from dotenv import load_dotenv
load_dotenv() load_dotenv()
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import func,or_
from sqlmodel import select
from pydantic import BaseModel from pydantic import BaseModel
class Email: class Email:
def __init__(self,session:AsyncSession,token): def __init__(self,session:AsyncSession,token=None):
self.session=session self.session=session
self.get_url=os.getenv("EMAIL_URL") self.get_url=os.getenv("EMAIL_URL")
self.token=token self.token=token
@ -41,4 +44,13 @@ class Email:
else: else:
raise HTTPException(status_code=response.status_code,detail=response.text) raise HTTPException(status_code=response.status_code,detail=response.text)
except Exception as e: except Exception as e:
raise HTTPException(status_code=500,detail=str(e)) raise HTTPException(status_code=500,detail=str(e))
async def get_inbox_messages(self,top,skip,search=None):
# Now here call the classmethod and return the result to app.py not removing the file or nbot rtouching the app.py code not touching any thin ion app.py only these two functions thjats it nothing else in views.py either pass
pass
async def count_inbox_messages(self,search=None):
# Now here call the classmethod and return the result to app.py not removing the file or nbot rtouching the app.py code not touching any thin ion app.py only these two functions thjats it nothing else in views.py either
pass

View File

@ -0,0 +1,34 @@
"""auto
Revision ID: a55e6b0a4d9a
Revises: ea9c09868aff
Create Date: 2026-08-03 15:20:16.015993+00:00
"""
from __future__ import annotations
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
import sqlmodel # SQLModel renders AutoString() into migrations but adds no import
revision: str = 'a55e6b0a4d9a'
down_revision: Union[str, None] = 'ea9c09868aff'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('inbox_messages', sa.Column('message_id', sqlmodel.sql.sqltypes.AutoString(), nullable=True), schema='app')
op.create_index(op.f('ix_inbox_messages_message_id'), 'inbox_messages', ['message_id'], unique=True, schema='app')
# ### end Alembic commands ###
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.drop_index(op.f('ix_inbox_messages_message_id'), table_name='inbox_messages', schema='app')
op.drop_column('inbox_messages', 'message_id', schema='app')
# ### end Alembic commands ###