# WildDuck Event Bus

The WildDuck Event Bus is an in-memory event system that provides observability and integration capabilities for all major operations across IMAP, LMTP, POP3, and API entry points.

## Overview

The event bus intercepts events from `lib/events.js` before they're queued to webhooks, providing real-time access to events through pluggable attachments. This allows you to build custom integrations, logging, monitoring, and analytics without modifying core WildDuck code.

### Key Features

- **In-memory only** - No database overhead
- **Asynchronous emission** - Non-blocking, fire-and-forget pattern using `setImmediate()`
- **Pluggable attachments** - Register custom event handlers
- **Automatic test integration** - Test tracker auto-registers in `NODE_ENV=test`
- **15 event types** - Covers messages, mailboxes, addresses, filters, spam, and settings
- **Zero breaking changes** - Webhooks continue to work unchanged

### Architecture

```
Services (API/IMAP/LMTP/POP3)
         │
         ▼
lib/events.js publish()
         │
         ├──> Event Bus (if shouldEmit)
         │         │
         │         ├──> Debug Logger Attachment
         │         ├──> Test Tracker Attachment
         │         └──> Custom Attachments
         │
         └──> Webhook Queue (original behavior)
```

## Event Types

WildDuck emits 15 event types across 6 categories:

### Address Events

| Event Type | Trigger | Data Fields |
|------------|---------|-------------|
| `address.user.created` | New email address created | `address`, `isDefault` |
| `address.user.deleted` | Email address deleted | `address` |

### Mailbox Events

| Event Type | Trigger | Data Fields |
|------------|---------|-------------|
| `mailbox.created` | Mailbox created | `path`, `specialUse` |
| `mailbox.renamed` | Mailbox renamed | `oldPath`, `newPath` |
| `mailbox.deleted` | Mailbox deleted | `path` |

### Message Events

| Event Type | Trigger | Data Fields |
|------------|---------|-------------|
| `message.added` | Message added (LMTP/IMAP/API) | `source`, `size`, `uid`, `mailboxPath` |
| `message.flags.changed` | Flags updated (IMAP STORE) | `uid`, `oldFlags`, `newFlags` |
| `message.moved` | Message moved (IMAP MOVE) | `fromMailbox`, `toMailbox`, `uid` |
| `message.deleted` | Message deleted (IMAP EXPUNGE) | `uid` |

**Message Sources:**
- `lmtp` - Incoming mail
- `imap` - IMAP APPEND
- `imap-copy` - IMAP COPY (same as added)
- `imap-move` - IMAP MOVE
- `api` - API submit/upload

### Settings Events

| Event Type | Trigger | Data Fields |
|------------|---------|-------------|
| `settings.updated` | User settings changed | `changedSettings: []` |
| `autoreply.user.enabled` | Autoreply enabled | `subject`, `message` |
| `autoreply.user.disabled` | Autoreply disabled | - |

### Filter Events

| Event Type | Trigger | Data Fields |
|------------|---------|-------------|
| `filter.created` | Filter created | `filterName`, `conditions` |
| `filter.deleted` | Filter deleted | `filterName` |

### Spam Events

| Event Type | Trigger | Data Fields |
|------------|---------|-------------|
| `marked.spam` | Message marked as spam | `message` |
| `marked.ham` | Message marked as ham | `message` |

## Event Structure

All events follow a normalized structure:

```javascript
{
  type: 'message.added',          // Event type (category.action)
  category: 'message',             // Category: message, mailbox, address, etc.
  action: 'added',                 // Action: created, updated, deleted, etc.
  timestamp: Date,                 // Event timestamp
  source: 'lmtp',                  // Source: api, imap, lmtp, etc.
  user: '507f1f77bcf86cd799439011', // User ID (string)

  // Optional fields (event-specific)
  mailbox: '507f1f77bcf86cd799439012', // Mailbox ID
  message: '507f1f77bcf86cd799439013', // Message ID

  // Event-specific data
  data: {
    size: 1024,
    uid: 42,
    mailboxPath: 'INBOX'
  },

  // Optional metadata (encryption, etc.)
  metadata: {
    encrypted: true
  }
}
```

## Using the Event Bus

### Importing the Event Bus

```javascript
const eventBus = require('./lib/event-bus');
```

The event bus is a singleton that auto-initializes on module load.

### Creating an Attachment

Attachments are plugins that receive events. Extend the `EventAttachment` base class:

```javascript
const EventAttachment = require('./lib/event-bus/attachment');

class MyAttachment extends EventAttachment {
  constructor() {
    super('my-attachment'); // Unique name
  }

  // Optional: Filter which events to handle
  shouldHandle(event) {
    // Only handle message events
    return event.category === 'message';
  }

  // Required: Handle the event
  async onEvent(event) {
    console.log('Event received:', event.type);
    console.log('User:', event.user);
    console.log('Data:', event.data);
  }

  // Optional: Cleanup on shutdown
  async onStop() {
    console.log('Attachment stopping...');
  }
}

module.exports = MyAttachment;
```

### Registering an Attachment

```javascript
const eventBus = require('./lib/event-bus');
const MyAttachment = require('./my-attachment');

const attachment = new MyAttachment();
eventBus.registerAttachment(attachment);
```

### Auto-Registration

For attachments that should always be active, use environment variables:

```javascript
// In lib/event-bus.js constructor
autoRegisterTestAttachments() {
  // Auto-register in test mode
  if (process.env.NODE_ENV === 'test') {
    const testTracker = require('./event-bus/attachments/test-tracker');
    this.registerAttachment(testTracker);
  }

  // Auto-register debug logger
  if (process.env.DEBUG_EVENTS) {
    const DebugLogger = require('./event-bus/attachments/debug-logger');
    this.registerAttachment(new DebugLogger());
  }
}
```

## Built-in Attachments

### Test Tracker

**Location:** `lib/event-bus/attachments/test-tracker.js`

**Purpose:** Store events in memory for test verification

**Auto-registers:** When `NODE_ENV=test`

**API:**
```javascript
const testTracker = require('./lib/event-bus/attachments/test-tracker');

// Clear events
testTracker.clear();

// Get events by type
const events = testTracker.getEventsByType('message.added');

// Get events by category
const messageEvents = testTracker.getEventsByCategory('message');

// Get events by source
const apiEvents = testTracker.getEventsBySource('api');

// Get last event
const lastEvent = testTracker.getLastEvent();

// Check if event was emitted
const hasEvent = testTracker.hasEventOfType('mailbox.created');

// Assert event was emitted (throws if not)
testTracker.assertEventEmitted('filter.created');

// Get summary statistics
const summary = testTracker.getSummary();
// Returns: { total: 10, byType: { 'user.created': 5, ... }, byCategory: {...} }

// Wait for event (with timeout)
const event = await testTracker.waitForEvent('message.added', 1000);
```

### Debug Logger

**Location:** `lib/event-bus/attachments/debug-logger.js`

**Purpose:** Log events to console for debugging

**Auto-registers:** When `DEBUG_EVENTS=1`

**Example:**
```bash
DEBUG_EVENTS=1 npm start
```

Output:
```
[Event Bus] message.added
  User: 507f1f77bcf86cd799439011
  Source: lmtp
  Data: { size: 1024, uid: 42 }
```

## Testing with Events

### Basic Test Pattern

```javascript
const testTracker = require('../lib/event-bus/attachments/test-tracker');

describe('My Test Suite', () => {
  it('should emit event when creating mailbox', async () => {
    // Clear tracker before test
    testTracker.clear();

    // Perform operation
    const response = await server.post('/users/123/mailboxes')
      .send({ path: 'Sent' });

    // Wait for async event emission
    await new Promise(resolve => setTimeout(resolve, 50));

    // Verify event
    const events = testTracker.getEventsByType('mailbox.created');
    expect(events).to.have.lengthOf(1);
    expect(events[0].category).to.equal('mailbox');
    expect(events[0].action).to.equal('created');
    expect(events[0].source).to.equal('api');
    expect(events[0].user).to.equal('123');
    expect(events[0].data.path).to.equal('Sent');
  });
});
```

### Testing Event Ordering

```javascript
it('should emit events in correct order', async () => {
  testTracker.clear();

  // Create user (should emit address and mailbox events)
  await createUser(server, { username: 'test' });

  await new Promise(resolve => setTimeout(resolve, 100));

  const events = testTracker.getAllEvents();

  // Verify address events come before mailbox events
  const addressIdx = events.findIndex(e => e.type === 'address.user.created');
  const mailboxIdx = events.findIndex(e => e.type === 'mailbox.created');

  expect(addressIdx).to.be.lessThan(mailboxIdx);
});
```

### Using waitForEvent

```javascript
it('should emit event within timeout', async () => {
  testTracker.clear();

  // Start async operation
  server.post('/users/123/mailboxes').send({ path: 'Drafts' });

  // Wait for event (throws if timeout)
  const event = await testTracker.waitForEvent('mailbox.created', 1000);

  expect(event.data.path).to.equal('Drafts');
});
```

## Example Attachments

See `docs/event-bus-examples.md` for complete examples:

1. **Metrics Collector** - Track event counts and timing
2. **External Webhook** - Forward events to external service
3. **Audit Logger** - Write events to log file
4. **Real-time Dashboard** - Stream events via WebSocket

## Performance Considerations

### Event Emission Overhead

- Events are emitted asynchronously using `setImmediate()`
- No blocking on main thread
- Typical overhead: <1ms per event
- Attachment errors don't affect core operations

### Memory Usage

- Events are only stored in test tracker (test mode only)
- Production mode has minimal memory footprint
- Debug logger writes to stdout (no storage)

### Scaling Recommendations

- Keep attachment logic lightweight
- Use async operations for heavy processing
- Avoid blocking operations in `onEvent()`
- Consider rate limiting for external APIs

## Configuration

### Enabling/Disabling Events

Events are controlled by the `EVENT_BUS_EVENTS` whitelist in `lib/events.js`:

```javascript
const EVENT_BUS_EVENTS = new Set([
  'address.user.created',
  'address.user.deleted',
  'mailbox.created',
  // ... all 15 events
]);
```

To disable an event type, remove it from this set.

### Environment Variables

- `NODE_ENV=test` - Auto-registers test tracker
- `DEBUG_EVENTS=1` - Auto-registers debug logger

## Best Practices

### DO

✅ Use `shouldHandle()` to filter events efficiently
✅ Handle errors gracefully in `onEvent()`
✅ Use async/await for async operations
✅ Clean up resources in `onStop()`
✅ Keep attachment logic simple and focused
✅ Test attachments thoroughly

### DON'T

❌ Don't block the event loop in `onEvent()`
❌ Don't throw errors (return/log instead)
❌ Don't modify event objects
❌ Don't rely on event ordering (except within same operation)
❌ Don't use events for critical business logic
❌ Don't store sensitive data in memory

## Troubleshooting

### Events Not Received

1. Check event type is in `EVENT_BUS_EVENTS` whitelist
2. Verify attachment is registered: `eventBus.getRegisteredAttachments()`
3. Check `shouldHandle()` is returning true
4. Enable debug logging: `DEBUG_EVENTS=1`

### Events Delayed

Events are emitted with `setImmediate()` for non-blocking behavior. In tests, wait 50-100ms:

```javascript
await new Promise(resolve => setTimeout(resolve, 50));
```

### Memory Leaks

Test tracker stores all events in memory. Clear regularly in tests:

```javascript
beforeEach(() => {
  testTracker.clear();
});
```

## API Reference

See `docs/event-bus-api.md` for complete API documentation.

## Migration Guide

### From Webhooks

If you're using webhooks, the event bus provides an alternative for in-process integrations:

**Before (Webhooks):**
```javascript
// Configure webhook in WildDuck settings
// Receive events via HTTP POST to external endpoint
```

**After (Event Bus):**
```javascript
// Create attachment
class MyAttachment extends EventAttachment {
  async onEvent(event) {
    // Process event in-process
  }
}

// Register attachment
eventBus.registerAttachment(new MyAttachment());
```

**When to use each:**
- **Webhooks:** External systems, decoupled architecture
- **Event Bus:** In-process monitoring, testing, local integrations

## Contributing

To add new event types:

1. Define constant in `lib/event-bus/event-types.js`
2. Add to `EVENT_BUS_EVENTS` in `lib/events.js`
3. Add `publish()` call at emission point
4. Update this documentation
5. Add integration tests

## License

Same as WildDuck - EUPL-1.2

## See Also

- [Event Bus API Reference](./event-bus-api.md)
- [Event Bus Examples](./event-bus-examples.md)
- [Architecture Documentation](../ARCHITECTURE.md)
- [Testing Guide](../TESTING.md)
