550 lines
8.4 KiB
JavaScript
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 );
|