From d662aea7e42376d833e196435cdcda08a208766c Mon Sep 17 00:00:00 2001 From: Narayanan Madaswamy Date: Thu, 22 Jan 2026 20:56:35 +0530 Subject: [PATCH] Mail box and OCR done --- backend/.env | 11 + backend/__pycache__/database.cpython-313.pyc | Bin 0 -> 3003 bytes .../__pycache__/mail_service.cpython-313.pyc | Bin 0 -> 4254 bytes backend/__pycache__/main.cpython-313.pyc | Bin 5168 -> 12009 bytes backend/__pycache__/scheduler.cpython-313.pyc | Bin 0 -> 949 bytes backend/create_tables.py | 12 + backend/database.py | 66 ++++++ backend/mail_service.py | 95 ++++++++ backend/main.py | 159 ++++++++++++- backend/requirements.txt | 5 + backend/scheduler.py | 14 ++ .../admin-layout/admin-layout.component.ts | 6 + frontend/src/app/app.routes.ts | 4 +- frontend/src/app/mailbox.service.ts | 59 +++++ frontend/src/app/mailbox/mailbox.component.ts | 222 ++++++++++++++++++ frontend/src/app/ocr/ocr.component.ts | 5 + 16 files changed, 644 insertions(+), 14 deletions(-) create mode 100644 backend/.env create mode 100644 backend/__pycache__/database.cpython-313.pyc create mode 100644 backend/__pycache__/mail_service.cpython-313.pyc create mode 100644 backend/__pycache__/scheduler.cpython-313.pyc create mode 100644 backend/create_tables.py create mode 100644 backend/database.py create mode 100644 backend/mail_service.py create mode 100644 backend/scheduler.py create mode 100644 frontend/src/app/mailbox.service.ts create mode 100644 frontend/src/app/mailbox/mailbox.component.ts diff --git a/backend/.env b/backend/.env new file mode 100644 index 0000000..e16f982 --- /dev/null +++ b/backend/.env @@ -0,0 +1,11 @@ +DB_USER=postgres +DB_PASSWORD=M@tr!x#149@dm!N +DB_HOST=192.168.0.111 +DB_PORT=7925 +DB_NAME=ocr + +# Mail Configuration (Gmail) +MAIL_SERVER=imap.gmail.com +MAIL_USERNAME=matrixinfotech.it@gmail.com +MAIL_PASSWORD=qtxsthbxbisgcmqu +MAIL_PORT=993 diff --git a/backend/__pycache__/database.cpython-313.pyc b/backend/__pycache__/database.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..5a00784c40447b767739e32b03aafc6c87587eab GIT binary patch literal 3003 zcmd5-O>7(25q``6T`swzNXe3G>&I4<(2hl*i7Y2|lt_`}gpy^KD>VVicCjR%=&i|J zYIf;FhEOb;ph#@AR#F&IZvuL;Z@IMa(MKOe5QVLm2C>nTZw}-`4?SewQX;LSMqBjK zB{*-sH#6^NX5P$hEG7~B(qF6^)i5D{=V1TyBnWo@;v?iHQHd(d6X2t7UdZ~um-T}` z8-PGI2*GR!LfJ5cvk{17MG&(RNJCuLKOfD?AbWgZK9-F`Jgb1>;oy8%HUWttl205X z<9queBDsV(@mspslM+B!YY)e+k|z240aLEr!PDW7{2seP0F zd|&R>jz{;o+{8ZD8S}}u)sg?gD%G|+sz&oi;zX76N0G+zJs#hSG@kE|lUF1d$or0w z{6O1cFdxSlE2@$oRJ&gD!%$xCWCaZO5tJql6SX@(JnnCu6pnebNt{hRGkaX^IZR-r zETnrMbH&B9$YMpHx~EHZ<-OGcHl z@U&%BsBSW8&H`$b&3U@Uq6<2d>9k?$u$C5BSBVxYI_QpZg=z)erfDCOZED+wWmfge zxVj9qf?~_GFBvtoh*m7Uq?Ig(npaqOW?EZTXLBry%*82HeeXhU#+!ZTg1Y1}7cb;m z%;MDf*{iXdWjke{_G;zzOoj=sGvO^JWaiR-Ch2v@Dq7X5;V_Z&E0wx^i3JLVSt@qI zOGs!B?7GO@#5V_mO2DTI;8%SR$OkbtA=Qt+0LCKh;RvGOYd#PUAjw0~b{u67$J#jV zVMPt8VYKbyuSkukqADTkR-=$C%jp;k&Q^7!@^}LRL(2d~|UD4|mhe=b8qZcn#sp+twXMI~>@q%8wtktYq9eq#j+n5I_7SS|Q zuTo88lBQLyQoX`?S<_bQdSy?dXxfSawo}1OF)b9wG|kZq721+R0Jn!AKdH|s0rw;e z+x5b)XwhLIo8ln>u$mHdyH#3)2xntJixl$%qmG9G(K9k^4YRTYagL&T`wlki(KOq^ zj45h33r3-i7tl0#5fyjwPuQuT6}78i7PvOiOSa7km6;zTzuL zmS7O+Rk4+PV;EV_X)ka&_xobH8(!id&B+N)PIB@xC+(DgQylW%$s*oL+J1^eR*Y5# zF}Y}A)iE8-S*uZqa$T7VwY_;SAAt8j;N{xez2(-^9P(p(gdg2rM8ab^{DW93Jsage zUcGg)Ie5CMOt|mt;Ka)HUn7(3-yHqJq`SbGlg;F)yYNWv+dTB;(T4oyN3*T1_$~(` z$gjvJcO8!z!YKZHuOE|k_cqA@SteJ=O<@SfoF7%c_jUSwnGFA1ofYzie~_#Q6aEz; zEiCfFsqQjELf~qSZ@X9D-~>hnjzpmVxcm1v{C`45WInZn@8?3amnArp6dhd z^b?;yn0yfLx)ymDk#0mjjr>mf$0xoP^G`d4%D;)85q=*yPCieZ5&qOY?*Fs@jF5hT z`7E1BSa|hH@e-}B0l(oh(dWg$d;8w2_nx_z46P_1?ih|?n;CniZ-9)JEdGpw1yzXg z<-F<;YBdW`xEjA5b`I#`;sht%oFutSu0y3_6vk>g*x2V<2>6Xyur(*G*0J$uHS7#b zre@J9VO6SQHDO)*sHWTY2Nsl=JTtX4H9e)yYRkC=EX-c_gh{?yU>a*n>y7ZV@Lc`9 zRArH~tr9y4KSdQDAA1t%6Tct`UlH*eV%;a!Uy1cKnfQi`e@zCy4G}^9IQCGIZ=|oK zH{NYZ18x}Obi=r2Y{r`LQ8)VFpzHwpiX^|4{-+fVuKrNzxv~D~`h8`zp^R>Q&{Q(6 z{2Ni#XIO}d_hNcZ68`lfknYI|&Q`_!9vR_`9VE8a`oo4A+W zR+qMy^V>_m*j9e&%8$g}jrFED;)ZsVA8pm|1nk6 literal 0 HcmV?d00001 diff --git a/backend/__pycache__/mail_service.cpython-313.pyc b/backend/__pycache__/mail_service.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b74b8a42caa2940bd8eed788afdc0873cf8d7d23 GIT binary patch literal 4254 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 literal 0 HcmV?d00001 diff --git a/backend/__pycache__/main.cpython-313.pyc b/backend/__pycache__/main.cpython-313.pyc index 82c5b366ee2c27389006632ff49d1060d043e68d..200e12e9f5aa7c61b6f0bee73d7b7754a66dd922 100644 GIT binary patch literal 12009 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 delta 1980 zcmaJ>O>7fa5PomH>-B%)n8YRy!AXGN{F0C#(o!UdfX0TTWuc|QRkXD?>4N{5-9Qtm zG9?mltyYz|RN~rRij+fp=%J?`svaDn3c3)rm3l#{)JSO$y>;d}fzU&rwBOFo%$u2S z-n@PO{=>tOt#CNNa0NTBo2AgFkwoo#&e&&6V;avgptqFe8408m$)2nyBZHh#K*@N) zn^8f{_`oM@a@L;-KtRY!Hkb)PNXXu7I1_=0ka@NVnlO%a>c8(M`ng4(8C+BTtW zMs0MLHjcSQUS_H7h+0doO^Z!Q+MZnNn54C~v#aupO807Qxppm{OODA}f-KOnOS4yK z_94&qY-eUa>_?ujBzch|nTu17oPs1e#A>T6~ ze9=`)wZH6N_aaYsJ*VKaY3};wsqTc+zP9$e65zbk`O{nT$I)Snyqp!K{}9Ab z63~f)6+*bnoXc;g zNzVdL`(|sdq8CbLc_mg@DwUQ8uimr=MUw|dMvtyEpCdXcZ7H9}jxQ_~(-IuSKtO#k zKSeGpv6=D>y=WFvc`yn_**5i}^@ZdHEnByjtob~$cfAF}*3DvChF03#Mj#GIMgAOJ+W zsiWR=tbXd^def!4@6xkC{K0hn$m#XKnYw)DA4|pvx5p>B8(1)l#{37%wqdzmFxLL^ zUF1CI%mhwGj-n6g^nwA<=iCqU@Gj@uKvJ1T!^qmRz>hpJfnwSZG_kH+sTyTCh9XQk z--Wuh<0Mk8Z~_78iinC?qFE&(a0)@xlET&C8mH0I^Y%Q=;QRtqO7o2kGw6H584uq+ zjPulzuas{XutrOzu04ug=1#J&pC5A0_N8j#%D#GUT|c#d9x4?Z-orhM_*fAHJo`{wS6$F?@~S4#fdkzI@Dd0fKJtifdkjsN%tE1P+|v?PDT;upXdbR3&+23A^kx|Su{RmvS zTCr>~yRIS*3Pd|a^QotOkD| zaL%7H{{~AYe_^M8Vy8FQ=mzV=@29L~gSGzV4X>Kc&jauFGfBCd_#|;};K9eY5_LYl vDS7x=e!pXj(R<6oq=|-VGvpi@JfQyF!gzYq=e#pGByCPOKMW=16sG+bWoN6d diff --git a/backend/__pycache__/scheduler.cpython-313.pyc b/backend/__pycache__/scheduler.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c6a8d6583f1296c00419c6f1bb577d9057aa5b06 GIT binary patch literal 949 zcmah|OK;Oa5Z<-dc~l84R0$6W)h!5N3B~16BoIQPoM=UYx^OBea_uE?n%HL7g{G%) z;ulo>1^5>nIi@02rB!?2ap?__KftV;q-i-Yl4r-WGvjZ*8Ta$~2?X-$;~W16M(C${ z8HzSA`gg(DMif!JjFvFRrn0qVZAs_4iC*G4v@i(egzA?NHz)y}su@=5P~BKqPA@v{ zdX0x0fk)js^ER4{gS+^e#csXo1YT9dA!k+Aa{Q)n2lbL816qftzX@s^ttL*TR+RUY ztdgOFVyaP{5^CHt%<-)BFyi z9k|s-xc1!AWm+&d3_Kw<-;-0EMNP+rmNtD6`$28%+EYj{N9~(5c6iJ@^ZJuP-TUUv zd0|~8W9I9pHW}IlO_9wU|iTO2yNTXc-UZWEbPkT6?+X> zXMtyr>Xo9loJ@*TM?O9cRZ{SXc~Bkni{$r6VTTlY#T%a*U*^8fbl1bK@31+9kL7Sx7oDFg2EBV2fbYSqMLP``DI c@d0{tfb8$NnQyt-PWBHW_)b5k;j0O%zd9q>fB*mh literal 0 HcmV?d00001 diff --git a/backend/create_tables.py b/backend/create_tables.py new file mode 100644 index 0000000..add618d --- /dev/null +++ b/backend/create_tables.py @@ -0,0 +1,12 @@ +from database import engine, Base + +def create_tables(): + print("Creating tables in database...") + try: + Base.metadata.create_all(bind=engine) + print("Tables created successfully!") + except Exception as e: + print(f"Error creating tables: {e}") + +if __name__ == "__main__": + create_tables() diff --git a/backend/database.py b/backend/database.py new file mode 100644 index 0000000..1467745 --- /dev/null +++ b/backend/database.py @@ -0,0 +1,66 @@ +import os +from sqlalchemy import create_engine, Column, Integer, String, Text, DateTime, Boolean, ForeignKey, LargeBinary +from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy.orm import sessionmaker, relationship +from dotenv import load_dotenv + +# Load environment variables +load_dotenv() + +DB_USER = os.getenv("DB_USER") +DB_PASSWORD = os.getenv("DB_PASSWORD") +DB_HOST = os.getenv("DB_HOST") +DB_PORT = os.getenv("DB_PORT") +import urllib.parse + +# ... (imports) + +# Load environment variables +load_dotenv() + +DB_USER = os.getenv("DB_USER") +DB_PASSWORD = os.getenv("DB_PASSWORD") +DB_HOST = os.getenv("DB_HOST") +DB_PORT = os.getenv("DB_PORT") +DB_NAME = os.getenv("DB_NAME") + +encoded_user = urllib.parse.quote_plus(DB_USER) +encoded_password = urllib.parse.quote_plus(DB_PASSWORD) + +# SQLAlchemy Database URL +DATABASE_URL = f"postgresql://{encoded_user}:{encoded_password}@{DB_HOST}:{DB_PORT}/{DB_NAME}" + +engine = create_engine(DATABASE_URL) +SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) + +Base = declarative_base() + +class Email(Base): + __tablename__ = "emails" + + id = Column(Integer, primary_key=True, index=True) + subject = Column(String, index=True) + sender = Column(String, index=True) + body = Column(Text) + received_date = Column(DateTime) + is_read = Column(Boolean, default=False) + + attachments = relationship("Attachment", back_populates="email") + +class Attachment(Base): + __tablename__ = "attachments" + + id = Column(Integer, primary_key=True, index=True) + email_id = Column(Integer, ForeignKey("emails.id")) + filename = Column(String) + content_type = Column(String) + file_content = Column(LargeBinary) # Storing content directly in DB as requested + + email = relationship("Email", back_populates="attachments") + +def get_db(): + db = SessionLocal() + try: + yield db + finally: + db.close() diff --git a/backend/mail_service.py b/backend/mail_service.py new file mode 100644 index 0000000..6827cb9 --- /dev/null +++ b/backend/mail_service.py @@ -0,0 +1,95 @@ +import os +import datetime +import mimetypes +import time +from imap_tools import MailBox, AND +from sqlalchemy.orm import Session +from database import SessionLocal, Email, Attachment +from dotenv import load_dotenv + +load_dotenv() + +MAIL_SERVER = os.getenv("MAIL_SERVER") +MAIL_USERNAME = os.getenv("MAIL_USERNAME") +MAIL_PASSWORD = os.getenv("MAIL_PASSWORD") +MAIL_PORT = int(os.getenv("MAIL_PORT", 993)) + +def fetch_and_store_emails(): + """ + Connects to Gmail, fetches UNREAD emails, stores them in DB, and marks as READ. + """ + print(f"[{datetime.datetime.now()}] Checking for new emails...") + + if not MAIL_USERNAME or not MAIL_PASSWORD or MAIL_USERNAME == "your-email@gmail.com": + print("Skipping Email Sync: Credentials not configured.") + return {"status": "skipped", "message": "Credentials not configured"} + + db: Session = SessionLocal() + count = 0 + + try: + # Connect to Inbox + # imap-tools MailBox defaults to SSL (port 993) if not specified, but we make it explicit here. + with MailBox(MAIL_SERVER, port=MAIL_PORT).login(MAIL_USERNAME, MAIL_PASSWORD, initial_folder='INBOX') as mailbox: + # Fetch UNREAD emails + for msg in mailbox.fetch(AND(seen=False)): + print(f"Processing Email: {msg.subject}") + + # Check if already exists (optional, but good practice if marking read fails) + # For now using simplistic approach: if we fetch it, we save it. + + new_email = Email( + subject=msg.subject, + sender=msg.from_, + body=msg.text or msg.html, + received_date=msg.date, + is_read=False # Consumed by app, but 'seen' on server + ) + db.add(new_email) + db.commit() # Commit to get ID + db.refresh(new_email) + + + # Process Attachments + for i, att in enumerate(msg.attachments): + filename = att.filename + if not filename: + # Fallback for missing filename + ext = mimetypes.guess_extension(att.content_type) or ".bin" + timestamp = int(time.time()) + filename = f"attachment_{timestamp}_{i}{ext}" + + print(f" - Attachment: {filename}") + new_attachment = Attachment( + email_id=new_email.id, + filename=filename, + content_type=att.content_type, + file_content=att.payload # raw bytes + ) + db.add(new_attachment) + + db.commit() + + # Mark as Read on Server (Auto-done by fetch unless bulk=True or mark_seen=False) + # imap-tools fetches are auto-seen by default. + count += 1 + + print(f"Sync Complete. {count} new emails.") + return {"status": "success", "count": count} + + except Exception as e: + error_msg = str(e) + print(f"Error fetching emails: {error_msg}") + + if "AUTHENTICATIONFAILED" in error_msg or "Invalid credentials" in error_msg: + return { + "status": "error", + "message": f"Authentication Failed: {error_msg}. \n[HINT]: If using Gmail, you MUST use an 'App Password', not your login password. Also ensure IMAP is enabled in Gmail Settings." + } + + return {"status": "error", "message": error_msg} + finally: + db.close() + +if __name__ == "__main__": + fetch_and_store_emails() diff --git a/backend/main.py b/backend/main.py index 7942579..02d793b 100644 --- a/backend/main.py +++ b/backend/main.py @@ -1,13 +1,32 @@ import io -from typing import Optional +import datetime +from typing import Optional, List +from contextlib import asynccontextmanager + from fastapi import FastAPI, File, UploadFile, HTTPException, Depends from fastapi.middleware.cors import CORSMiddleware from pydantic import BaseModel from pypdf import PdfReader import pytesseract from PIL import Image +from pdf2image import convert_from_bytes +from sqlalchemy.orm import Session -app = FastAPI() +# Internal modules +from database import get_db, Email +from scheduler import start_scheduler, stop_scheduler +from mail_service import fetch_and_store_emails + +# Lifespan for Scheduler +@asynccontextmanager +async def lifespan(app: FastAPI): + # Startup + start_scheduler() + yield + # Shutdown + stop_scheduler() + +app = FastAPI(lifespan=lifespan) # CORS configuration origins = [ @@ -38,13 +57,6 @@ class LoginResponse(BaseModel): class NERResponse(BaseModel): text: str -# 1. User Login Module (Dummy) -@app.post("/api/login", response_model=LoginResponse) -async def login(request: LoginRequest): - if request.username == "admin" and request.password == "admin": - return LoginResponse(token="dummy-jwt-token-123", message="Login Successful") - raise HTTPException(status_code=401, detail="Invalid credentials") - # 2. OCR Module def extract_text_from_pdf(file_bytes: bytes) -> str: try: @@ -68,10 +80,6 @@ def extract_text_from_image(file_bytes: bytes) -> str: print(f"Error reading Image: {e}") return "" -from pdf2image import convert_from_bytes - -# ... (imports) - @app.post("/api/ocr/extract", response_model=NERResponse) async def extract_text(file: UploadFile = File(...)): content = await file.read() @@ -100,6 +108,131 @@ async def extract_text(file: UploadFile = File(...)): return NERResponse(text=extracted_text) +import zipfile +import mimetypes +from fastapi.responses import StreamingResponse + +class EmailDTO(BaseModel): + id: int + subject: Optional[str] + sender: Optional[str] + received_date: Optional[datetime.datetime] + is_read: bool + has_attachments: bool = False + +class AttachmentDTO(BaseModel): + id: int + filename: str + content_type: str + +class EmailListResponse(BaseModel): + items: List[EmailDTO] + total: int + +class EmailDetailDTO(EmailDTO): + body: Optional[str] + attachments: List[AttachmentDTO] + +@app.get("/api/emails", response_model=EmailListResponse) +def get_emails(skip: int = 0, limit: int = 50, db: Session = Depends(get_db)): + total = db.query(Email).count() + emails = db.query(Email).order_by(Email.received_date.desc()).offset(skip).limit(limit).all() + + items = [ + EmailDTO( + id=e.id, + subject=e.subject, + sender=e.sender, + received_date=e.received_date, + is_read=e.is_read, + has_attachments=bool(e.attachments) # Check if list is not empty + ) for e in emails + ] + return EmailListResponse(items=items, total=total) + +# ... + +@app.get("/api/emails/{email_id}", response_model=EmailDetailDTO) +def get_email_details(email_id: int, db: Session = Depends(get_db)): + email = db.query(Email).filter(Email.id == email_id).first() + if not email: + raise HTTPException(status_code=404, detail="Email not found") + + # Mark as read if opened + if not email.is_read: + email.is_read = True + db.commit() + + # Safely handle missing content_type and filename + attachments = [] + for a in email.attachments: + fname = a.filename + if not fname or fname == "unknown": + ext = mimetypes.guess_extension(a.content_type or "") or ".bin" + fname = f"attachment_{a.id}{ext}" + + attachments.append( + AttachmentDTO( + id=a.id, + filename=fname, + content_type=a.content_type or "application/octet-stream" + ) + ) + + return EmailDetailDTO( + id=email.id, + subject=email.subject, + sender=email.sender, + received_date=email.received_date, + is_read=email.is_read, + has_attachments=bool(email.attachments), + body=email.body, + attachments=attachments + ) + +@app.get("/api/emails/{email_id}/download-all") +def download_all_attachments(email_id: int, db: Session = Depends(get_db)): + email = db.query(Email).filter(Email.id == email_id).first() + if not email: + raise HTTPException(status_code=404, detail="Email not found") + + if not email.attachments: + raise HTTPException(status_code=400, detail="No attachments found") + + # Create a zip file in memory + zip_buffer = io.BytesIO() + with zipfile.ZipFile(zip_buffer, "w", zipfile.ZIP_DEFLATED) as zip_file: + for att in email.attachments: + # Use filename from DB, ensure unique names if duplicates exist (simple append here) + zip_file.writestr(att.filename, att.file_content) + + zip_buffer.seek(0) + + return StreamingResponse( + zip_buffer, + media_type="application/zip", + headers={"Content-Disposition": f"attachment; filename=email-{email_id}-attachments.zip"} + ) + +@app.get("/api/attachments/{attachment_id}") +def download_attachment(attachment_id: int, view: bool = False, db: Session = Depends(get_db)): + attachment = db.query(DBAttachment).filter(DBAttachment.id == attachment_id).first() + if not attachment: + raise HTTPException(status_code=404, detail="Attachment not found") + + disposition = "inline" if view else "attachment" + + return StreamingResponse( + io.BytesIO(attachment.file_content), + media_type=attachment.content_type, + headers={"Content-Disposition": f"{disposition}; filename={attachment.filename}"} + ) + +@app.post("/api/emails/sync") +def sync_emails(): + result = fetch_and_store_emails() + return result + @app.get("/") def read_root(): return {"message": "OCR Backend API is running"} diff --git a/backend/requirements.txt b/backend/requirements.txt index 56c20f2..9f0da77 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -4,3 +4,8 @@ python-multipart pypdf pytesseract Pillow +sqlalchemy +psycopg2-binary +imap-tools +apscheduler +python-dotenv diff --git a/backend/scheduler.py b/backend/scheduler.py new file mode 100644 index 0000000..63e3b2c --- /dev/null +++ b/backend/scheduler.py @@ -0,0 +1,14 @@ +from apscheduler.schedulers.background import BackgroundScheduler +from mail_service import fetch_and_store_emails + +scheduler = BackgroundScheduler() + +def start_scheduler(): + # Add job to run every 10 seconds + scheduler.add_job(fetch_and_store_emails, 'interval', seconds=10, id='mail_sync_job', replace_existing=True) + scheduler.start() + print("Mail Scheduler Started (Interval: 10s)") + +def stop_scheduler(): + scheduler.shutdown() + print("Mail Scheduler Stopped") diff --git a/frontend/src/app/admin-layout/admin-layout.component.ts b/frontend/src/app/admin-layout/admin-layout.component.ts index aa5f5a7..37df214 100644 --- a/frontend/src/app/admin-layout/admin-layout.component.ts +++ b/frontend/src/app/admin-layout/admin-layout.component.ts @@ -51,6 +51,12 @@ export class AdminLayoutComponent implements OnInit { icon: 'pi pi-history' } ] + + }, + { + label: 'Mailbox', + icon: 'pi pi-envelope', + routerLink: '/mailbox' }, { label: 'Settings', diff --git a/frontend/src/app/app.routes.ts b/frontend/src/app/app.routes.ts index aff1440..8199137 100644 --- a/frontend/src/app/app.routes.ts +++ b/frontend/src/app/app.routes.ts @@ -2,6 +2,7 @@ import { Routes } from '@angular/router'; import { LoginComponent } from './login/login.component'; import { AdminLayoutComponent } from './admin-layout/admin-layout.component'; import { OcrComponent } from './ocr/ocr.component'; +import { MailboxComponent } from './mailbox/mailbox.component'; import { AuthGuard } from './auth.guard'; export const routes: Routes = [ @@ -12,7 +13,8 @@ export const routes: Routes = [ canActivate: [AuthGuard], children: [ { path: '', redirectTo: 'ocr', pathMatch: 'full' }, - { path: 'ocr', component: OcrComponent } + { path: 'ocr', component: OcrComponent }, + { path: 'mailbox', component: MailboxComponent } ] }, { path: '**', redirectTo: '' } diff --git a/frontend/src/app/mailbox.service.ts b/frontend/src/app/mailbox.service.ts new file mode 100644 index 0000000..31d424a --- /dev/null +++ b/frontend/src/app/mailbox.service.ts @@ -0,0 +1,59 @@ +import { Injectable } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import { Observable } from 'rxjs'; + +export interface AttachmentDTO { + id: number; + filename: string; + content_type: string; +} + +export interface EmailDTO { + id: number; + subject: string; + sender: string; + received_date: string; + is_read: boolean; + has_attachments?: boolean; // New flag from backend + body?: string; + attachments?: AttachmentDTO[]; +} + +export interface EmailListResponse { + items: EmailDTO[]; + total: number; +} + +@Injectable({ + providedIn: 'root' +}) +export class MailboxService { + private apiUrl = 'http://localhost:8000/api/emails'; + private attachmentUrl = 'http://localhost:8000/api/attachments'; + + constructor(private http: HttpClient) {} + + getEmails(skip: number = 0, limit: number = 50): Observable { + return this.http.get(`${this.apiUrl}?skip=${skip}&limit=${limit}`); + } + + getEmailDetails(id: number): Observable { + return this.http.get(`${this.apiUrl}/${id}`); + } + + getAttachmentUrl(id: number): string { + return `${this.attachmentUrl}/${id}`; + } + + getPreviewUrl(id: number): string { + return `${this.attachmentUrl}/${id}?view=true`; + } + + getZipDownloadUrl(emailId: number): string { + return `${this.apiUrl}/${emailId}/download-all`; + } + + syncEmails(): Observable { + return this.http.post(`${this.apiUrl}/sync`, {}); + } +} diff --git a/frontend/src/app/mailbox/mailbox.component.ts b/frontend/src/app/mailbox/mailbox.component.ts new file mode 100644 index 0000000..15a004a --- /dev/null +++ b/frontend/src/app/mailbox/mailbox.component.ts @@ -0,0 +1,222 @@ +import { Component, OnInit } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { TableModule, TableLazyLoadEvent } from 'primeng/table'; +import { ButtonModule } from 'primeng/button'; +import { ToastModule } from 'primeng/toast'; +import { ProgressBarModule } from 'primeng/progressbar'; +import { DialogModule } from 'primeng/dialog'; +import { MessageService } from 'primeng/api'; +import { MailboxService, EmailDTO } from '../mailbox.service'; + +@Component({ + selector: 'app-mailbox', + standalone: true, + imports: [ + CommonModule, + TableModule, + ButtonModule, + ToastModule, + ProgressBarModule, + DialogModule + ], + providers: [MessageService], + template: ` +
+
+

Mailbox

+ +
+ + + + + + + ID + + Sender + Subject + Date + Status + Action + + + + + {{email.id}} + + + + {{email.sender}} + {{email.subject}} + {{email.received_date | date:'short'}} + + + + + + + + + + + No emails found. Click Sync to fetch from Gmail. + + + + + + + +
+
+ From: {{selectedEmail.sender}}
+ Subject: {{selectedEmail.subject}}
+ Date: {{selectedEmail.received_date | date:'medium'}}
+
+
+ + +
+
+

Attachments ({{selectedEmail.attachments.length}})

+ + Download All + +
+ +
+
+ + +
+ + + +
+ + + + + + + + + {{att.filename}} + +
+
+
+
+
+
+ `, + styles: [` + .font-bold { font-weight: 700; } + .email-body { max-height: 400px; overflow-y: auto; background: #f9f9f9; padding: 1rem; border-radius: 4px; } + .attachment-card:hover .download-overlay { opacity: 1; } + .download-overlay { transition: opacity 0.2s; } + `] +}) +export class MailboxComponent implements OnInit { + emails: EmailDTO[] = []; + totalRecords: number = 0; + loading: boolean = true; + syncing: boolean = false; + + displayDialog: boolean = false; + selectedEmail: EmailDTO | null = null; + + constructor( + private mailboxService: MailboxService, + private messageService: MessageService + ) {} + + ngOnInit() { + // Initial load handled by p-table lazy load + } + + loadEmails(event: TableLazyLoadEvent) { + this.loading = true; + const skip = event.first || 0; + const limit = event.rows || 50; + + this.mailboxService.getEmails(skip, limit).subscribe({ + next: (res) => { + this.emails = res.items; + this.totalRecords = res.total; + this.loading = false; + }, + error: (err) => { + console.error(err); + this.loading = false; + } + }); + } + + viewEmail(id: number) { + this.mailboxService.getEmailDetails(id).subscribe({ + next: (email) => { + this.selectedEmail = email; + this.displayDialog = true; + // Update local read status + const idx = this.emails.findIndex(e => e.id === id); + if(idx !== -1) { + this.emails[idx].is_read = true; + } + }, + error: (err) => this.messageService.add({severity:'error', summary:'Error', detail:'Could not load email details'}) + }); + } + + getDownloadLink(id: number): string { + return this.mailboxService.getAttachmentUrl(id); + } + + getPreviewLink(id: number): string { + return this.mailboxService.getPreviewUrl(id); + } + + getDownloadAllLink(emailId: number): string { + return this.mailboxService.getZipDownloadUrl(emailId); + } + + isImage(contentType: string): boolean { + return contentType.startsWith('image/'); + } + + isPdf(contentType: string): boolean { + return contentType === 'application/pdf'; + } + + sync() { + this.syncing = true; + this.mailboxService.syncEmails().subscribe({ + next: (res) => { + this.syncing = false; + this.messageService.add({severity:'success', summary:'Sync Complete', detail: res.message || 'Emails synced successfully'}); + // Refresh table + this.loadEmails({first: 0, rows: 50}); + }, + error: (err) => { + this.syncing = false; + this.messageService.add({severity:'error', summary:'Sync Failed', detail: 'Could not fetch emails'}); + } + }); + } +} diff --git a/frontend/src/app/ocr/ocr.component.ts b/frontend/src/app/ocr/ocr.component.ts index 75fab9c..192a56d 100644 --- a/frontend/src/app/ocr/ocr.component.ts +++ b/frontend/src/app/ocr/ocr.component.ts @@ -35,6 +35,7 @@ import { ToastModule } from 'primeng/toast'; cancelLabel="Clear" [customUpload]="true" (uploadHandler)="onUpload($event)" + (onClear)="onClear()" accept=".pdf,image/*" maxFileSize="10000000"> @@ -84,4 +85,8 @@ export class OcrComponent { } }); } + + onClear() { + this.extractedText = null; + } }