@push.rocks/smartmta 8.1.0 → 8.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -30,6 +30,22 @@ export interface IActionExecutorDeps {
30
30
  deliveryQueue: UnifiedDeliveryQueue;
31
31
  }
32
32
 
33
+ export interface IActionExecutionOptions {
34
+ /**
35
+ * Queue the action with a deterministic durable id instead of performing
36
+ * direct delivery. Used when replaying an already-accepted envelope.
37
+ */
38
+ durableQueue?: {
39
+ idempotencyKey: string;
40
+ fingerprint: string;
41
+ };
42
+ }
43
+
44
+ export interface IActionExecutionResult {
45
+ outcome: 'completed' | 'queued';
46
+ queueId?: string;
47
+ }
48
+
33
49
  /**
34
50
  * Executes email routing actions (forward, process, deliver, reject)
35
51
  */
@@ -61,30 +77,45 @@ export class EmailActionExecutor {
61
77
  }
62
78
  }
63
79
 
64
- async executeAction(action: IEmailAction, email: Email, context: IEmailContext): Promise<void> {
80
+ async executeAction(
81
+ action: IEmailAction,
82
+ email: Email,
83
+ context: IEmailContext,
84
+ ): Promise<void> {
85
+ await this.executeActionWithResult(action, email, context);
86
+ }
87
+
88
+ async executeActionWithResult(
89
+ action: IEmailAction,
90
+ email: Email,
91
+ context: IEmailContext,
92
+ options: IActionExecutionOptions = {},
93
+ ): Promise<IActionExecutionResult> {
65
94
  switch (action.type) {
66
95
  case 'forward':
67
- await this.handleForwardAction(action, email, context);
68
- break;
96
+ return this.handleForwardAction(action, email, context, options);
69
97
  case 'process':
70
- await this.handleProcessAction(action, email, context);
71
- break;
98
+ return this.handleProcessAction(action, email, context, options);
72
99
  case 'deliver':
73
- await this.handleDeliverAction(action, email, context);
74
- break;
100
+ return this.handleDeliverAction(action, email, context, options);
75
101
  case 'store':
76
102
  throw new Error(
77
103
  'Store actions require raw SMTP context and an awaited hooks.onAcceptEnvelope handler',
78
104
  );
79
105
  case 'reject':
80
106
  await this.handleRejectAction(action, email, context);
81
- break;
107
+ throw new Error('Reject action unexpectedly returned');
82
108
  default:
83
109
  throw new Error(`Unknown action type: ${(action as any).type}`);
84
110
  }
85
111
  }
86
112
 
87
- private async handleForwardAction(action: IEmailAction, email: Email, context: IEmailContext): Promise<void> {
113
+ private async handleForwardAction(
114
+ action: IEmailAction,
115
+ email: Email,
116
+ context: IEmailContext,
117
+ options: IActionExecutionOptions,
118
+ ): Promise<IActionExecutionResult> {
88
119
  if (!action.forward) {
89
120
  throw new Error('Forward action requires forward configuration');
90
121
  }
@@ -105,6 +136,20 @@ export class EmailActionExecutor {
105
136
  email.headers['X-Forwarded-To'] = email.to.join(', ');
106
137
  email.headers['X-Forwarded-Date'] = new Date().toISOString();
107
138
 
139
+ if (options.durableQueue) {
140
+ const queueId = await this.deps.deliveryQueue.enqueue(
141
+ email,
142
+ 'forward',
143
+ context.session.matchedRoute!,
144
+ {
145
+ idempotencyKey: options.durableQueue.idempotencyKey,
146
+ idempotencyFingerprint: options.durableQueue.fingerprint,
147
+ requireDurable: true,
148
+ },
149
+ );
150
+ return { outcome: 'queued', queueId };
151
+ }
152
+
108
153
  try {
109
154
  // Send email via Rust SMTP client
110
155
  const result = await this.deps.sendOutboundEmail(host, port, email, {
@@ -128,6 +173,7 @@ export class EmailActionExecutor {
128
173
  },
129
174
  success: true
130
175
  }));
176
+ return { outcome: 'completed' };
131
177
  } catch (error) {
132
178
  const errorMessage = getErrorMessage(error);
133
179
  logger.log('error', `Failed to forward email: ${errorMessage}`);
@@ -172,7 +218,12 @@ export class EmailActionExecutor {
172
218
  }
173
219
  }
174
220
 
175
- private async handleProcessAction(action: IEmailAction, email: Email, context: IEmailContext): Promise<void> {
221
+ private async handleProcessAction(
222
+ action: IEmailAction,
223
+ email: Email,
224
+ context: IEmailContext,
225
+ options: IActionExecutionOptions,
226
+ ): Promise<IActionExecutionResult> {
176
227
  logger.log('info', `Processing email with action options`);
177
228
 
178
229
  // Apply scanning if requested
@@ -182,18 +233,47 @@ export class EmailActionExecutor {
182
233
 
183
234
  // Queue for delivery
184
235
  const queue = action.process?.queue || 'normal';
185
- await this.deps.deliveryQueue.enqueue(email, 'process', context.session.matchedRoute!);
236
+ const queueId = await this.deps.deliveryQueue.enqueue(
237
+ email,
238
+ 'process',
239
+ context.session.matchedRoute!,
240
+ options.durableQueue
241
+ ? {
242
+ idempotencyKey: options.durableQueue.idempotencyKey,
243
+ idempotencyFingerprint: options.durableQueue.fingerprint,
244
+ requireDurable: true,
245
+ }
246
+ : {},
247
+ );
186
248
 
187
249
  logger.log('info', `Email queued for delivery in ${queue} queue`);
250
+ return { outcome: 'queued', queueId };
188
251
  }
189
252
 
190
- private async handleDeliverAction(_action: IEmailAction, email: Email, context: IEmailContext): Promise<void> {
253
+ private async handleDeliverAction(
254
+ _action: IEmailAction,
255
+ email: Email,
256
+ context: IEmailContext,
257
+ options: IActionExecutionOptions,
258
+ ): Promise<IActionExecutionResult> {
191
259
  logger.log('info', `Delivering email locally`);
192
260
 
193
261
  // Queue for local delivery
194
- await this.deps.deliveryQueue.enqueue(email, 'mta', context.session.matchedRoute!);
262
+ const queueId = await this.deps.deliveryQueue.enqueue(
263
+ email,
264
+ 'mta',
265
+ context.session.matchedRoute!,
266
+ options.durableQueue
267
+ ? {
268
+ idempotencyKey: options.durableQueue.idempotencyKey,
269
+ idempotencyFingerprint: options.durableQueue.fingerprint,
270
+ requireDurable: true,
271
+ }
272
+ : {},
273
+ );
195
274
 
196
275
  logger.log('info', 'Email queued for local delivery');
276
+ return { outcome: 'queued', queueId };
197
277
  }
198
278
 
199
279
  private async handleRejectAction(action: IEmailAction, _email: Email, context: IEmailContext): Promise<void> {