import express from "express"; import Imap from "imap"; import { simpleParser } from "mailparser"; import { promisify } from "util"; import { once } from "events"; import data from "./data.json" with { type: "json" }; class ProtonMailIMAPReader { constructor ( config ) { this.imap = new Imap( config ); this.imap.once( "error", this.onImapError.bind( this ) ); this.imap.once( "end", this.onImapEnd.bind( this ) ); this.openBoxAsync = promisify( this.imap.openBox.bind( this.imap ) ); this.getBoxesAsync = promisify( this.imap.getBoxes.bind( this.imap ) ); } onImapError ( err ) { console.error( "IMAP Error:", err ); } onImapEnd ( ) { console.log( "Connection ended" ); } connect ( ) { this.imap.connect( ); return once( this.imap, "ready" ); } openBox ( boxName ) { return this.openBoxAsync( boxName, false ); } async fetchEmails ( folder = "INBOX" ) { const box = await this.openBox( folder ); if ( !box || box.messages.total === 0 ) { return new Array( ); } let range; if ( box.messages.total > 9 ) { range = ( box.messages.total - 9 ) + ":" + box.messages.total; } else { range = "1:" + box.messages.total; } const fetch = this.imap.seq.fetch( range, { bodies: "", struct: true } ); const messageBuffers = { }; fetch.on( "message", this.handleMessage.bind( this, messageBuffers ) ); await once( fetch, "end" ); const emails = await this.parseMessages( messageBuffers ); emails.sort( this.compareEmailsBySeqno.bind( this ) ); return emails; } async parseMessages ( messageBuffers ) { const emails = new Array( ); const seqnos = Object.keys( messageBuffers ); for ( let i = 0 ; i < seqnos.length ; i ++ ) { const seqno = seqnos[ i ]; const parsed = await simpleParser( messageBuffers[ seqno ] ); const email = this.prepareEmailObject( parseInt( seqno ), parsed ); emails.push( email ); } return emails; } compareEmailsBySeqno ( a, b ) { if ( a.seqno < b.seqno ) { return -1; } if ( a.seqno > b.seqno ) { return 1; } return 0; } handleMessage ( buffers, msg, seqno ) { const bufferChunks = new Array( ); msg.on( "body", this.onMessageBody.bind( this, bufferChunks ) ); msg.once( "end", this.onMessageEnd.bind( this, buffers, seqno, bufferChunks ) ); } onMessageBody ( bufferChunks, stream ) { stream.on( "data", this.onStreamData.bind( this, bufferChunks ) ); } onStreamData ( bufferChunks, chunk ) { bufferChunks.push( chunk ); } onMessageEnd ( buffers, seqno, bufferChunks ) { buffers[ seqno ] = Buffer.concat( bufferChunks ); } async fetchEmailBySeqno ( seqno, folder = "INBOX" ) { await this.openBox( folder ); const fetch = this.imap.seq.fetch( String( seqno ), { bodies: "", struct: true } ); const messageBuffers = { }; fetch.on( "message", this.handleMessage.bind( this, messageBuffers ) ); await once( fetch, "end" ); if ( !messageBuffers[ seqno ] ) { return null; } const parsed = await simpleParser( messageBuffers[ seqno ] ); return this.prepareEmailObject( parseInt( seqno ), parsed ); } prepareEmailObject ( seqno, parsed ) { const email = { seqno, subject: parsed.subject ?? "(no subject)", from: parsed.from?.text ?? "(unknown sender)", date: parsed.date, text: parsed.text ?? "" }; return email; } async markAsRead ( seqno, folder = "INBOX" ) { await this.openBox( folder ); return this.addFlagsAsync( seqno, "\\Seen" ); } async removeMessage ( seqno, folder = "INBOX" ) { await this.openBox( folder ); const success = await this.addFlagsAsync( seqno, "\\Deleted" ); if ( !success ) { return false; } return this.expungeAsync( seqno ); } addFlagsAsync ( seqno, flag ) { return new Promise( ( resolve ) => { this.imap.seq.addFlags( seqno, flag, this.onAddFlagsDone.bind( this, resolve ) ); } ); } onAddFlagsDone ( resolve, err ) { if ( err ) { console.error( "Error adding flags:", err ); } resolve( !err ); } expungeAsync ( seqno ) { return new Promise( ( resolve ) => { this.imap.seq.expunge( seqno, this.onExpungeDone.bind( this, resolve ) ); } ); } onExpungeDone ( resolve, err ) { if ( err ) { console.error( "Error expunging message:", err ); } resolve( !err ); } async listFolders ( ) { const boxes = await this.getBoxesAsync( ); const flat = new Array( ); this.flattenBoxes( boxes, "", flat ); return flat; } flattenBoxes ( boxes, prefix, flat ) { for ( const name in boxes ) { const path = prefix ? prefix + "/" + name : name; flat.push( path ); if ( boxes[ name ].children ) { this.flattenBoxes( boxes[ name ].children, path, flat ); } } } end ( ) { this.imap.end( ); } } class ProtonMailServer { constructor ( ) { this.app = express( ); this.app.get( "/emails", this.getEmails.bind( this ) ); this.app.use( "/emails/folder/", this.getFolderEmails.bind( this ) ); this.app.get( "/message/:index", this.getEmailByIndex.bind( this ) ); this.app.get( "/flag/read/:index", this.markEmailAsRead.bind( this ) ); this.app.get( "/removeMessage/:message_id", this.removeMessageById.bind( this ) ); this.app.get( "/folders", this.getFolders.bind( this ) ); } formatJsonResponse ( res, data ) { const jsonString = JSON.stringify( data, null, "\t" ); res.setHeader( "Content-Type", "application/json" ); res.send( jsonString ); } async getEmails ( req, res ) { const reader = new ProtonMailIMAPReader( data ); await reader.connect( ); const emails = await reader.fetchEmails( ); reader.end( ); const filtered = emails.map( this.filterEmailSummary ); this.formatJsonResponse( res, { type: "emails", data: filtered, message: "Emails have been fetched." } ); } filterEmailSummary ( email ) { const summary = { seqno: email.seqno, subject: email.subject, from: email.from, date: email.date }; return summary; } async getFolderEmails ( req, res ) { let folderPath = decodeURIComponent( req.path.replace( "/emails/folder/", "" ) ); if ( folderPath.startsWith( "/" ) ) { folderPath = folderPath.slice( 1 ); } const reader = new ProtonMailIMAPReader( data ); await reader.connect( ); let emails; try { emails = await reader.fetchEmails( folderPath ); } catch ( err ) { reader.end( ); res.status( 500 ).json( { error: "Failed to fetch emails for folder: " + folderPath } ); return; } reader.end( ); const filtered = emails.map( this.filterEmailSummary ); this.formatJsonResponse( res, { type: "emails", folder: folderPath, data: filtered, message: "Emails have been fetched." } ); } async getEmailByIndex ( req, res ) { const seqno = req.params.index; const reader = new ProtonMailIMAPReader( data ); await reader.connect( ); const email = await reader.fetchEmailBySeqno( seqno ); reader.end( ); if ( email === null ) { res.status( 404 ).json( { error: "Email not found" } ); } else { this.formatJsonResponse( res, { type: "email", data: email, message: "Email fetched successfully." } ); } } async markEmailAsRead ( req, res ) { const seqno = req.params.index; const reader = new ProtonMailIMAPReader( data ); await reader.connect( ); await reader.markAsRead( seqno ); reader.end( ); this.formatJsonResponse( res, { message: "Email marked as read." } ); } async removeMessageById ( req, res ) { const seqno = req.params.message_id; const reader = new ProtonMailIMAPReader( data ); await reader.connect( ); const success = await reader.removeMessage( seqno ); reader.end( ); if ( success ) { this.formatJsonResponse( res, { message: "Email removed successfully." } ); } else { this.formatJsonResponse( res, { error: "Failed to remove email." } ); } } async getFolders ( req, res ) { const reader = new ProtonMailIMAPReader( data ); await reader.connect( ); const flatFolders = await reader.listFolders( ); reader.end( ); this.formatJsonResponse( res, { folders: flatFolders } ); } start ( port = 3000 ) { this.app.listen( port, this.onServerStarted.bind( this, port ) ); } onServerStarted ( port ) { console.log( "ProtonMail IMAP server running on port " + port ); } } const server = new ProtonMailServer( ); server.start( 3000 );