feat(core): Introduce ImapClient and ImapServer classes for enhanced IMAP support

This commit is contained in:
Philipp Kunz 2024-11-26 22:58:26 +01:00
parent d3cc3ef9a5
commit 9c42210acd
9 changed files with 3706 additions and 506 deletions

View File

@ -1,5 +1,13 @@
# Changelog # Changelog
## 2024-11-26 - 1.2.0 - feat(core)
Introduce ImapClient and ImapServer classes for enhanced IMAP support
- Implemented ImapClient class for managing IMAP connections and message retrieval.
- Implemented ImapServer class to simulate an IMAP server for testing.
- Added new tests for ImapClient and ImapServer to ensure reliability.
- Updated dependencies in package.json to latest versions.
## 2024-09-19 - 1.1.0 - feat(core) ## 2024-09-19 - 1.1.0 - feat(core)
Enhance package with detailed documentation and updated npm metadata Enhance package with detailed documentation and updated npm metadata

View File

@ -14,17 +14,17 @@
"buildDocs": "(tsdoc)" "buildDocs": "(tsdoc)"
}, },
"devDependencies": { "devDependencies": {
"@git.zone/tsbuild": "^2.1.25", "@git.zone/tsbuild": "^2.2.0",
"@git.zone/tsbundle": "^2.0.5", "@git.zone/tsbundle": "^2.1.0",
"@git.zone/tsrun": "^1.2.46", "@git.zone/tsrun": "^1.3.3",
"@git.zone/tstest": "^1.0.44", "@git.zone/tstest": "^1.0.44",
"@push.rocks/tapbundle": "^5.3.0", "@push.rocks/tapbundle": "^5.5.3",
"@types/node": "^22.5.5" "@types/node": "^22.10.0"
}, },
"dependencies": { "dependencies": {
"@types/imapflow": "^1.0.19", "@types/imapflow": "^1.0.19",
"@types/mailparser": "^3.4.4", "@types/mailparser": "^3.4.5",
"imapflow": "^1.0.164", "imapflow": "^1.0.169",
"mailparser": "^3.7.1" "mailparser": "^3.7.1"
}, },
"repository": { "repository": {

3976
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@ -2,10 +2,10 @@ import { expect, expectAsync, tap } from '@push.rocks/tapbundle';
import { tapNodeTools } from '@push.rocks/tapbundle/node'; import { tapNodeTools } from '@push.rocks/tapbundle/node';
import * as smartimap from '../ts/index.js'; import * as smartimap from '../ts/index.js';
let testSmartImap: smartimap.SmartImap; let testSmartImap: smartimap.ImapClient;
tap.test('smartimap', async () => { tap.test('smartimap', async () => {
testSmartImap = new smartimap.SmartImap({ testSmartImap = new smartimap.ImapClient({
host: await tapNodeTools.getEnvVarOnDemand('IMAP_URL'), host: await tapNodeTools.getEnvVarOnDemand('IMAP_URL'),
port: 993, port: 993,
secure: true, secure: true,
@ -13,14 +13,14 @@ tap.test('smartimap', async () => {
user: await tapNodeTools.getEnvVarOnDemand('IMAP_USER'), user: await tapNodeTools.getEnvVarOnDemand('IMAP_USER'),
pass: await tapNodeTools.getEnvVarOnDemand('IMAP_PASSWORD'), pass: await tapNodeTools.getEnvVarOnDemand('IMAP_PASSWORD'),
}, },
mailbox: 'buchhaltung', mailbox: 'INBOX',
filter: { seen: true, to: await tapNodeTools.getEnvVarOnDemand('IMAP_USER'), }, filter: { seen: true, to: await tapNodeTools.getEnvVarOnDemand('IMAP_USER'), },
}); });
await testSmartImap.connect(); await testSmartImap.connect();
testSmartImap.on('message', (message) => { testSmartImap.on('message', (message: smartimap.SmartImapMessage) => {
console.log(message); console.log(message.subject);
}); });
testSmartImap.on('error', (error) => { testSmartImap.on('error', (error) => {

29
test/test.imapserver.ts Normal file
View File

@ -0,0 +1,29 @@
import { tap, expect, expectAsync } from '@push.rocks/tapbundle';
import { jestExpect } from '@push.rocks/tapbundle/node';
import { ImapServer } from '../ts/classes.imapserver.js';
tap.test('imapserver', async () => {
// Example usage
const imapServer = new ImapServer();
imapServer.addUser('testuser', 'password');
imapServer.createInbox('testuser', 'INBOX');
imapServer.createInbox('testuser', 'Sent');
// Add a sample message
const testUser = imapServer.users.get('testuser')!;
const inbox = testUser.inboxes.get('INBOX')!;
inbox.messages.push({
id: '1',
subject: 'Welcome',
sender: 'no-reply@example.com',
recipient: 'testuser@example.com',
date: new Date(),
body: 'Welcome to your new IMAP inbox!',
});
// Start the server on port 143 (commonly used for IMAP)
// imapServer.start(143);
});
tap.start();

View File

@ -3,6 +3,6 @@
*/ */
export const commitinfo = { export const commitinfo = {
name: '@push.rocks/smartimap', name: '@push.rocks/smartimap',
version: '1.1.0', version: '1.2.0',
description: 'A Node.js library for event-driven streaming and parsing of IMAP email messages.' description: 'A Node.js library for event-driven streaming and parsing of IMAP email messages.'
} }

View File

@ -1,6 +1,6 @@
import * as plugins from './smartimap.plugins.js'; import * as plugins from './smartimap.plugins.js';
export interface SmartImapConfig { export interface ImapClientConfig {
host: string; host: string;
port?: number; // Defaults to 993 if secure, else 143 port?: number; // Defaults to 993 if secure, else 143
secure?: boolean; // Defaults to true secure?: boolean; // Defaults to true
@ -12,7 +12,9 @@ export interface SmartImapConfig {
filter?: plugins.imapflow.SearchObject; // IMAP search criteria object filter?: plugins.imapflow.SearchObject; // IMAP search criteria object
} }
export class SmartImap extends plugins.events.EventEmitter { export type SmartImapMessage = plugins.mailparser.ParsedMail;
export class ImapClient extends plugins.events.EventEmitter {
private client: plugins.imapflow.ImapFlow; private client: plugins.imapflow.ImapFlow;
private mailbox: string; private mailbox: string;
private filter: plugins.imapflow.SearchObject; private filter: plugins.imapflow.SearchObject;
@ -20,7 +22,7 @@ export class SmartImap extends plugins.events.EventEmitter {
private processing: boolean = false; private processing: boolean = false;
private seenUids: Set<number> = new Set(); private seenUids: Set<number> = new Set();
constructor(private config: SmartImapConfig) { constructor(private config: ImapClientConfig) {
super(); super();
this.mailbox = config.mailbox || 'INBOX'; this.mailbox = config.mailbox || 'INBOX';

162
ts/classes.imapserver.ts Normal file
View File

@ -0,0 +1,162 @@
import * as net from "net";
export interface IImapServerMessage {
id: string;
subject: string;
sender: string;
recipient: string;
date: Date;
body: string;
}
export interface IImapServerInbox {
name: string;
messages: IImapServerMessage[];
}
export interface IImapServerUser {
username: string;
password: string;
inboxes: Map<string, IImapServerInbox>;
}
export class ImapServer {
public users: Map<string, IImapServerUser>;
private server: net.Server;
constructor() {
this.users = new Map();
this.server = net.createServer(this.handleConnection.bind(this));
}
// Add a user for authentication
public addUser(username: string, password: string): void {
if (this.users.has(username)) {
throw new Error(`User "${username}" already exists.`);
}
this.users.set(username, { username, password, inboxes: new Map() });
}
// Add an inbox for a user
public createInbox(username: string, inboxName: string): void {
const user = this.users.get(username);
if (!user) {
throw new Error(`User "${username}" does not exist.`);
}
if (user.inboxes.has(inboxName)) {
throw new Error(`Inbox "${inboxName}" already exists for user "${username}".`);
}
user.inboxes.set(inboxName, { name: inboxName, messages: [] });
}
// Start the server
public start(port: number): void {
this.server.listen(port, () => {
console.log(`IMAP Server started on port ${port}`);
});
}
// Stop the server
public stop(): void {
this.server.close(() => {
console.log("IMAP Server stopped.");
});
}
// Handle a new client connection
private handleConnection(socket: net.Socket): void {
let currentUser: IImapServerUser | null = null;
let selectedInbox: IImapServerInbox | null = null;
socket.write("* OK IMAP4rev1 Service Ready\r\n");
socket.on("data", (data) => {
const command = data.toString().trim();
console.log(`Received command: ${command}`);
const [tag, keyword, ...args] = command.split(" ");
let response = "";
try {
switch (keyword.toUpperCase()) {
case "LOGIN": {
const [username, password] = args;
const user = this.users.get(username);
if (user && user.password === password) {
currentUser = user;
response = `${tag} OK LOGIN completed`;
} else {
response = `${tag} NO LOGIN failed`;
}
break;
}
case "LIST": {
if (!currentUser) {
response = `${tag} NO Not authenticated`;
break;
}
const inboxNames = Array.from(currentUser.inboxes.keys()).map((inbox) => `* LIST () "/" ${inbox}`);
response = `${inboxNames.join("\r\n")}\r\n${tag} OK LIST completed`;
break;
}
case "SELECT": {
if (!currentUser) {
response = `${tag} NO Not authenticated`;
break;
}
const inboxName = args[0];
const inbox = currentUser.inboxes.get(inboxName);
if (inbox) {
selectedInbox = inbox;
response = `* FLAGS (\\Answered \\Flagged \\Deleted \\Seen \\Draft)\r\n* EXISTS ${inbox.messages.length}\r\n${tag} OK [READ-WRITE] SELECT completed`;
} else {
response = `${tag} NO SELECT failed: No such mailbox`;
}
break;
}
case "FETCH": {
if (!selectedInbox) {
response = `${tag} NO No mailbox selected`;
break;
}
const [id] = args;
const message = selectedInbox.messages.find((msg) => msg.id === id);
if (message) {
response = `* ${id} FETCH (BODY[TEXT] {${message.body.length}}\r\n${message.body}\r\n)\r\n${tag} OK FETCH completed`;
} else {
response = `${tag} NO FETCH failed: No such message`;
}
break;
}
case "LOGOUT": {
response = `* BYE IMAP4rev1 Server logging out\r\n${tag} OK LOGOUT completed`;
socket.write(response + "\r\n");
socket.end();
return;
}
default: {
response = `${tag} BAD Unknown command`;
break;
}
}
} catch (error) {
response = `${tag} BAD Error: ${error.message}`;
}
socket.write(response + "\r\n");
});
socket.on("close", () => {
console.log("Client disconnected.");
});
socket.on("error", (err) => {
console.error("Socket error:", err);
});
}
}

View File

@ -1 +1,2 @@
export * from './classes.smartimap.js'; export * from './classes.imapclient.js';
export * from './classes.imapserver.js';