First commit
This commit is contained in:
549
email-server.js
Normal file
549
email-server.js
Normal file
@@ -0,0 +1,549 @@
|
||||
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 );
|
||||
Reference in New Issue
Block a user