feat(core): Introduce ImapClient and ImapServer classes for enhanced IMAP support
This commit is contained in:
		| @@ -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 | ||||||
|  |  | ||||||
|   | |||||||
							
								
								
									
										14
									
								
								package.json
									
									
									
									
									
								
							
							
						
						
									
										14
									
								
								package.json
									
									
									
									
									
								
							| @@ -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
									
									
									
								
							
							
						
						
									
										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 { 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
									
								
							
							
						
						
									
										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 = { | 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.' | ||||||
| } | } | ||||||
|   | |||||||
| @@ -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
									
								
							
							
						
						
									
										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