feat(core): Introduce ImapClient and ImapServer classes for enhanced IMAP support
This commit is contained in:
		@@ -1,5 +1,13 @@
 | 
			
		||||
# 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)
 | 
			
		||||
Enhance package with detailed documentation and updated npm metadata
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										14
									
								
								package.json
									
									
									
									
									
								
							
							
						
						
									
										14
									
								
								package.json
									
									
									
									
									
								
							@@ -14,17 +14,17 @@
 | 
			
		||||
    "buildDocs": "(tsdoc)"
 | 
			
		||||
  },
 | 
			
		||||
  "devDependencies": {
 | 
			
		||||
    "@git.zone/tsbuild": "^2.1.25",
 | 
			
		||||
    "@git.zone/tsbundle": "^2.0.5",
 | 
			
		||||
    "@git.zone/tsrun": "^1.2.46",
 | 
			
		||||
    "@git.zone/tsbuild": "^2.2.0",
 | 
			
		||||
    "@git.zone/tsbundle": "^2.1.0",
 | 
			
		||||
    "@git.zone/tsrun": "^1.3.3",
 | 
			
		||||
    "@git.zone/tstest": "^1.0.44",
 | 
			
		||||
    "@push.rocks/tapbundle": "^5.3.0",
 | 
			
		||||
    "@types/node": "^22.5.5"
 | 
			
		||||
    "@push.rocks/tapbundle": "^5.5.3",
 | 
			
		||||
    "@types/node": "^22.10.0"
 | 
			
		||||
  },
 | 
			
		||||
  "dependencies": {
 | 
			
		||||
    "@types/imapflow": "^1.0.19",
 | 
			
		||||
    "@types/mailparser": "^3.4.4",
 | 
			
		||||
    "imapflow": "^1.0.164",
 | 
			
		||||
    "@types/mailparser": "^3.4.5",
 | 
			
		||||
    "imapflow": "^1.0.169",
 | 
			
		||||
    "mailparser": "^3.7.1"
 | 
			
		||||
  },
 | 
			
		||||
  "repository": {
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										3976
									
								
								pnpm-lock.yaml
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										3976
									
								
								pnpm-lock.yaml
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							@@ -2,10 +2,10 @@ import { expect, expectAsync, tap } from '@push.rocks/tapbundle';
 | 
			
		||||
import { tapNodeTools } from '@push.rocks/tapbundle/node';
 | 
			
		||||
import * as smartimap from '../ts/index.js';
 | 
			
		||||
 | 
			
		||||
let testSmartImap: smartimap.SmartImap;
 | 
			
		||||
let testSmartImap: smartimap.ImapClient;
 | 
			
		||||
 | 
			
		||||
tap.test('smartimap', async () => {
 | 
			
		||||
  testSmartImap = new smartimap.SmartImap({
 | 
			
		||||
  testSmartImap = new smartimap.ImapClient({
 | 
			
		||||
    host: await tapNodeTools.getEnvVarOnDemand('IMAP_URL'),
 | 
			
		||||
    port: 993,
 | 
			
		||||
    secure: true,
 | 
			
		||||
@@ -13,14 +13,14 @@ tap.test('smartimap', async () => {
 | 
			
		||||
      user: await tapNodeTools.getEnvVarOnDemand('IMAP_USER'),
 | 
			
		||||
      pass: await tapNodeTools.getEnvVarOnDemand('IMAP_PASSWORD'),
 | 
			
		||||
    },
 | 
			
		||||
    mailbox: 'buchhaltung',
 | 
			
		||||
    mailbox: 'INBOX',
 | 
			
		||||
    filter: { seen: true, to: await tapNodeTools.getEnvVarOnDemand('IMAP_USER'), },
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  await testSmartImap.connect();
 | 
			
		||||
 | 
			
		||||
  testSmartImap.on('message', (message) => {
 | 
			
		||||
    console.log(message);
 | 
			
		||||
  testSmartImap.on('message', (message: smartimap.SmartImapMessage) => {
 | 
			
		||||
    console.log(message.subject);
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  testSmartImap.on('error', (error) => {
 | 
			
		||||
							
								
								
									
										29
									
								
								test/test.imapserver.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								test/test.imapserver.ts
									
									
									
									
									
										Normal 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();
 | 
			
		||||
@@ -3,6 +3,6 @@
 | 
			
		||||
 */
 | 
			
		||||
export const commitinfo = {
 | 
			
		||||
  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.'
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,6 @@
 | 
			
		||||
import * as plugins from './smartimap.plugins.js';
 | 
			
		||||
 | 
			
		||||
export interface SmartImapConfig {
 | 
			
		||||
export interface ImapClientConfig {
 | 
			
		||||
    host: string;
 | 
			
		||||
    port?: number; // Defaults to 993 if secure, else 143
 | 
			
		||||
    secure?: boolean; // Defaults to true
 | 
			
		||||
@@ -12,7 +12,9 @@ export interface SmartImapConfig {
 | 
			
		||||
    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 mailbox: string;
 | 
			
		||||
    private filter: plugins.imapflow.SearchObject;
 | 
			
		||||
@@ -20,7 +22,7 @@ export class SmartImap extends plugins.events.EventEmitter {
 | 
			
		||||
    private processing: boolean = false;
 | 
			
		||||
    private seenUids: Set<number> = new Set();
 | 
			
		||||
 | 
			
		||||
    constructor(private config: SmartImapConfig) {
 | 
			
		||||
    constructor(private config: ImapClientConfig) {
 | 
			
		||||
        super();
 | 
			
		||||
 | 
			
		||||
        this.mailbox = config.mailbox || 'INBOX';
 | 
			
		||||
							
								
								
									
										162
									
								
								ts/classes.imapserver.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										162
									
								
								ts/classes.imapserver.ts
									
									
									
									
									
										Normal 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);
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@@ -1 +1,2 @@
 | 
			
		||||
export * from './classes.smartimap.js';
 | 
			
		||||
export * from './classes.imapclient.js';
 | 
			
		||||
export * from './classes.imapserver.js';
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user