main
js 500 lines 14.9 KB
Raw
1 import { createStore } from "/js/AlpineStore.js";
2 import * as API from "/js/api.js";
3
4 const API_BASE = "/plugins/_email_integration";
5 const GMAIL_APP_PASSWORDS_URL = "https://support.google.com/mail/answer/185833?hl=en";
6 const PRESETS = {
7 "": {
8 label: "Choose a provider",
9 account_type: "imap",
10 imap_server: "",
11 imap_port: 993,
12 smtp_server: "",
13 smtp_port: 587,
14 },
15 gmail: {
16 label: "Gmail",
17 account_type: "imap",
18 imap_server: "imap.gmail.com",
19 imap_port: 993,
20 smtp_server: "smtp.gmail.com",
21 smtp_port: 587,
22 },
23 icloud: {
24 label: "iCloud Mail",
25 account_type: "imap",
26 imap_server: "imap.mail.me.com",
27 imap_port: 993,
28 smtp_server: "smtp.mail.me.com",
29 smtp_port: 587,
30 },
31 microsoft365: {
32 label: "Outlook / Microsoft 365",
33 account_type: "imap",
34 imap_server: "outlook.office365.com",
35 imap_port: 993,
36 smtp_server: "smtp.office365.com",
37 smtp_port: 587,
38 },
39 yahoo: {
40 label: "Yahoo Mail",
41 account_type: "imap",
42 imap_server: "imap.mail.yahoo.com",
43 imap_port: 993,
44 smtp_server: "smtp.mail.yahoo.com",
45 smtp_port: 587,
46 },
47 exchange: {
48 label: "Exchange",
49 account_type: "exchange",
50 imap_server: "outlook.office365.com",
51 imap_port: 993,
52 smtp_server: "smtp.office365.com",
53 smtp_port: 587,
54 },
55 "custom-imap": {
56 label: "Custom IMAP",
57 account_type: "imap",
58 imap_server: "",
59 imap_port: 993,
60 smtp_server: "",
61 smtp_port: 587,
62 },
63 };
64
65 const PROVIDER_OPTIONS = [
66 {
67 value: "gmail",
68 label: "Gmail",
69 hint: "Google app password",
70 logo: "gmail",
71 logoText: "M",
72 },
73 {
74 value: "icloud",
75 label: "iCloud Mail",
76 hint: "Apple app-specific password",
77 logo: "icloud",
78 logoText: "cloud",
79 },
80 {
81 value: "microsoft365",
82 label: "Outlook / Microsoft 365",
83 hint: "Microsoft mailbox preset",
84 logo: "microsoft365",
85 logoText: "",
86 },
87 {
88 value: "yahoo",
89 label: "Yahoo Mail",
90 hint: "Yahoo app password",
91 logo: "yahoo",
92 logoText: "Y!",
93 },
94 {
95 value: "exchange",
96 label: "Exchange",
97 hint: "Organization Exchange server",
98 logo: "exchange",
99 logoText: "X",
100 },
101 {
102 value: "custom-imap",
103 label: "Custom IMAP",
104 hint: "Manual server settings",
105 logo: "custom-imap",
106 logoText: "dns",
107 },
108 ];
109
110 function ensureConfig(config) {
111 if (!config || typeof config !== "object") return;
112 if (!Array.isArray(config.handlers)) config.handlers = [];
113 }
114
115 export const store = createStore("emailConfig", {
116 config: null,
117 context: null,
118 editing: null,
119 testing: null,
120 testResults: null,
121 testResultsFor: null,
122 providerPickerOpen: null,
123 guideOpen: false,
124 didInit: false,
125 projects: [],
126 modelPresets: [],
127 presets: PRESETS,
128
129 get providerOptions() {
130 return PROVIDER_OPTIONS;
131 },
132
133 get handlers() {
134 ensureConfig(this.config);
135 return Array.isArray(this.config?.handlers) ? this.config.handlers : [];
136 },
137
138 async init(config, context = null) {
139 if (!config || typeof config !== "object") return;
140
141 this.config = config || null;
142 this.context = context;
143 this.didInit = false;
144 ensureConfig(this.config);
145 this.editing = this.handlers.length === 1 ? 0 : null;
146 this.testing = null;
147 this.testResults = null;
148 this.testResultsFor = null;
149 this.providerPickerOpen = null;
150 this.guideOpen = this.handlers.length === 0 && window.innerWidth > 720;
151 if (this.handlers.length === 0) this._startInitialHandlerFlow();
152 if (this.editing !== null && !this.hasProvider(this.handlers[this.editing])) {
153 this.providerPickerOpen = this.editing;
154 }
155 this.didInit = true;
156
157 const [projectsResult, presetsResult] = await Promise.allSettled([
158 API.callJsonApi("projects", { action: "list" }),
159 API.callJsonApi("/plugins/_model_config/model_presets", { action: "get" }),
160 ]);
161
162 this.projects = projectsResult.status === "fulfilled" ? (projectsResult.value.data || []) : [];
163 this.modelPresets = presetsResult.status === "fulfilled" && Array.isArray(presetsResult.value.presets)
164 ? presetsResult.value.presets
165 : [];
166 },
167
168 cleanup() {
169 this.config = null;
170 this.context = null;
171 this.editing = null;
172 this.testing = null;
173 this.testResults = null;
174 this.testResultsFor = null;
175 this.providerPickerOpen = null;
176 this.guideOpen = false;
177 this.didInit = false;
178 },
179
180 newHandler() {
181 return {
182 name: "",
183 enabled: false,
184 provider: "",
185 account_type: "imap",
186 imap_server: "",
187 imap_port: 993,
188 smtp_server: "",
189 smtp_port: 587,
190 username: "",
191 password: "",
192 poll_mode: "seconds",
193 poll_interval_seconds: 60,
194 poll_interval_cron: "*/2 * * * *",
195 process_unread_days: 0,
196 sender_whitelist: [],
197 project: "",
198 dispatcher_model: "utility",
199 chat_model_preset: "",
200 dispatcher_instructions: "",
201 agent_instructions: "",
202 };
203 },
204
205 addHandler() {
206 ensureConfig(this.config);
207 this.config.handlers.push(this.newHandler());
208 this.editing = this.config.handlers.length - 1;
209 this.providerPickerOpen = this.editing;
210 this.testResults = null;
211 this.testResultsFor = null;
212 },
213
214 removeHandler(idx) {
215 this.handlers.splice(idx, 1);
216 if (this.editing === idx) this.editing = null;
217 if (this.editing !== null && this.editing > idx) this.editing -= 1;
218 if (this.providerPickerOpen === idx) this.providerPickerOpen = null;
219 if (this.providerPickerOpen !== null && this.providerPickerOpen > idx) this.providerPickerOpen -= 1;
220 if (this.testResultsFor === idx) {
221 this.testResults = null;
222 this.testResultsFor = null;
223 }
224 },
225
226 toggleEditing(idx) {
227 this.editing = this.editing === idx ? null : idx;
228 if (this.editing === null) {
229 this.providerPickerOpen = null;
230 } else if (!this.hasProvider(this.handlers[idx])) {
231 this.providerPickerOpen = idx;
232 }
233 if (this.testResultsFor !== idx) {
234 this.testResults = null;
235 this.testResultsFor = null;
236 }
237 },
238
239 providerValue(handler) {
240 if (!handler || typeof handler !== "object") return "";
241 const explicitProvider = String(handler.provider || "").trim();
242 if (explicitProvider && this.presets[explicitProvider]) return explicitProvider;
243
244 const incoming = String(handler.imap_server || "").trim().toLowerCase();
245 const outgoing = String(handler.smtp_server || "").trim().toLowerCase();
246 const accountType = handler.account_type || "imap";
247
248 if (!incoming && !outgoing && !handler.username && !handler.password) return "";
249 if (accountType === "exchange") return "exchange";
250 if (incoming === "imap.gmail.com" || outgoing === "smtp.gmail.com") return "gmail";
251 if (incoming === "imap.mail.me.com" || outgoing === "smtp.mail.me.com") return "icloud";
252 if (incoming === "outlook.office365.com" || outgoing === "smtp.office365.com") return "microsoft365";
253 if (incoming === "imap.mail.yahoo.com" || outgoing === "smtp.mail.yahoo.com") return "yahoo";
254 return "custom-imap";
255 },
256
257 hasProvider(handler) {
258 return Boolean(this.providerValue(handler));
259 },
260
261 showProviderChoices(handler, idx) {
262 return !this.hasProvider(handler) || this.providerPickerOpen === idx;
263 },
264
265 changeProvider(idx) {
266 this.providerPickerOpen = this.providerPickerOpen === idx ? null : idx;
267 },
268
269 providerLabel(value) {
270 return this.presets[value]?.label || "Custom IMAP";
271 },
272
273 selectProvider(handler, value, idx = null) {
274 this.applyProvider(handler, value);
275 this.providerPickerOpen = null;
276 this.testResults = null;
277 this.testResultsFor = null;
278 if (idx !== null) this.editing = idx;
279 },
280
281 applyProvider(handler, value) {
282 const preset = this.presets[value];
283 if (!preset) return;
284 const previous = this.providerValue(handler);
285
286 handler.account_type = preset.account_type;
287 handler.provider = value;
288
289 if (value === "custom-imap") {
290 if (previous !== "custom-imap") {
291 handler.imap_server = "";
292 handler.smtp_server = "";
293 }
294 if (!handler.imap_port) handler.imap_port = 993;
295 if (!handler.smtp_port) handler.smtp_port = 587;
296 return;
297 }
298
299 handler.imap_server = preset.imap_server;
300 handler.imap_port = preset.imap_port;
301 handler.smtp_server = preset.smtp_server;
302 handler.smtp_port = preset.smtp_port;
303 },
304
305 providerHint(handler) {
306 const provider = this.providerValue(handler);
307 if (provider === "gmail") return "Use a Google App Password. A regular Gmail password usually will not work here.";
308 if (provider === "icloud") return "Use an app-specific password from your Apple account settings.";
309 if (provider === "microsoft365") return "Most Outlook and Microsoft 365 inboxes work with this preset.";
310 if (provider === "yahoo") return "Yahoo Mail usually works best with an app password.";
311 if (provider === "exchange") return "Choose Exchange only if your organization requires it. For the simplest setup, try Outlook / Microsoft 365 first.";
312 if (provider === "custom-imap") return "Bring your own incoming and outgoing mail server details.";
313 return "How to start: turn on inbox, pick your provider, then add your email address and password.";
314 },
315
316 providerHelpUrl(handler) {
317 return this.providerValue(handler) === "gmail" ? GMAIL_APP_PASSWORDS_URL : "";
318 },
319
320 providerHelpLabel(handler) {
321 if (this.providerValue(handler) !== "gmail") return "";
322 return "Google's guide to create a Gmail App Password";
323 },
324
325 showManualServers(handler) {
326 return this.providerValue(handler) === "custom-imap";
327 },
328
329 showExchangeServer(handler) {
330 return this.providerValue(handler) === "exchange";
331 },
332
333 incomingLabel(handler) {
334 return this.showExchangeServer(handler) ? "Exchange server" : "Incoming mail server";
335 },
336
337 incomingDescription(handler) {
338 return this.showExchangeServer(handler)
339 ? "The Exchange or Microsoft 365 server for this inbox"
340 : "The IMAP server for incoming mail";
341 },
342
343 incomingPlaceholder(handler) {
344 return this.showExchangeServer(handler) ? "outlook.office365.com" : "imap.your-provider.com";
345 },
346
347 scheduleLabel(handler) {
348 const value = this.frequencyValue(handler);
349 if (value === "15") return "Checks every 15 seconds";
350 if (value === "30") return "Checks every 30 seconds";
351 if (value === "60") return "Checks every minute";
352 if (value === "300") return "Checks every 5 minutes";
353 if (value === "900") return "Checks every 15 minutes";
354 return "Uses a custom schedule";
355 },
356
357 frequencyValue(handler) {
358 if (handler.poll_mode !== "seconds") return "custom";
359 const seconds = Number(handler.poll_interval_seconds || 0);
360 if ([15, 30, 60, 300, 900].includes(seconds)) return String(seconds);
361 return "custom";
362 },
363
364 applyFrequency(handler, value) {
365 if (value === "custom") {
366 if (handler.poll_mode !== "cron") handler.poll_mode = "seconds";
367 if (!handler.poll_interval_seconds) handler.poll_interval_seconds = 60;
368 return;
369 }
370 handler.poll_mode = "seconds";
371 handler.poll_interval_seconds = Number(value);
372 },
373
374 frequencyHint(handler) {
375 return this.frequencyValue(handler) === "custom"
376 ? "You are using a custom schedule. You can change the raw timing in Advanced."
377 : this.scheduleLabel(handler);
378 },
379
380 whitelistText(handler) {
381 return (handler.sender_whitelist || []).join(", ");
382 },
383
384 setWhitelist(handler, value) {
385 handler.sender_whitelist = value
386 .split(",")
387 .map((entry) => entry.trim())
388 .filter((entry) => entry);
389 },
390
391 slugify(value) {
392 return String(value || "")
393 .toLowerCase()
394 .replace(/[^a-z0-9]+/g, "_")
395 .replace(/^_+|_+$/g, "");
396 },
397
398 maybeAutoname(handler) {
399 const current = String(handler.name || "").trim();
400 if (current && !/^handler_\d+$/.test(current)) return;
401 const email = String(handler.username || "").trim();
402 const localPart = email.split("@")[0] || "";
403 const nextName = this.slugify(localPart);
404 if (nextName) handler.name = nextName;
405 },
406
407 missingBits(handler) {
408 const missing = [];
409 const provider = this.providerValue(handler);
410 if (!provider) missing.push("provider");
411 if (!handler.username) missing.push("email address");
412 if (!handler.password) missing.push("password");
413 if ((this.showManualServers(handler) || this.showExchangeServer(handler)) && !handler.imap_server) {
414 missing.push(this.showExchangeServer(handler) ? "Exchange server" : "incoming server");
415 }
416 if (this.showManualServers(handler) && !handler.smtp_server) missing.push("outgoing server");
417 return missing;
418 },
419
420 canTest(handler) {
421 return this.missingBits(handler).length === 0;
422 },
423
424 statusLabel(handler) {
425 if (!handler.username && !this.providerValue(handler)) return "New";
426 if (handler.enabled && this.canTest(handler)) return "Live";
427 if (this.canTest(handler)) return "Ready";
428 return "Needs info";
429 },
430
431 statusTone(handler) {
432 if (!handler.username && !this.providerValue(handler)) return "muted";
433 if (handler.enabled && this.canTest(handler)) return "success";
434 if (this.canTest(handler)) return "ready";
435 return "warning";
436 },
437
438 handlerTitle(handler, idx) {
439 return handler.username || handler.name || `Inbox ${idx + 1}`;
440 },
441
442 handlerSubtitle(handler) {
443 const pieces = [];
444 const provider = this.providerValue(handler);
445 if (provider) pieces.push(this.providerLabel(provider));
446 pieces.push(this.scheduleLabel(handler).replace("Checks ", ""));
447 if (handler.project) pieces.push(`Project: ${handler.project}`);
448 return pieces.join(" · ");
449 },
450
451 testButtonLabel(handler, idx) {
452 if (this.testing === idx) return "Checking...";
453 if (this.canTest(handler)) return "Check setup";
454 return "Fill in the basics first";
455 },
456
457 testIntro(handler) {
458 if (this.providerValue(handler) === "exchange") {
459 return "We will check the inbox, check outgoing mail, then send a test email to this address.";
460 }
461 return "We will check incoming mail, check outgoing mail, then send a test email to this inbox.";
462 },
463
464 resultTitle(result) {
465 return result.test || "Check";
466 },
467
468 resultMessage(result) {
469 return result.message || (result.ok ? "Done." : "Something went wrong.");
470 },
471
472 _startInitialHandlerFlow() {
473 this.addHandler();
474 if (!this.context) return;
475 const toComparableJson = typeof this.context._toComparableJson === "function"
476 ? this.context._toComparableJson.bind(this.context)
477 : JSON.stringify;
478 this.context.settingsSnapshotJson = toComparableJson(this.context.settings);
479 },
480
481 async testConnection(idx) {
482 const handler = this.handlers[idx];
483 if (!handler || !this.canTest(handler)) return;
484
485 this.testing = idx;
486 this.testResults = null;
487 this.testResultsFor = idx;
488
489 try {
490 this.testResults = await API.callJsonApi(`${API_BASE}/test_connection`, { handler });
491 } catch (error) {
492 this.testResults = {
493 success: false,
494 results: [{ test: "Connection", ok: false, message: String(error) }],
495 };
496 }
497
498 this.testing = null;
499 },
500 });