Files
Proton-Api/email-server.js
2025-12-30 20:28:13 +01:00

550 lines
8.4 KiB
JavaScript

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 );