nothumanallowed 13.5.116 → 13.5.118
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.
- package/package.json +1 -1
- package/src/constants.mjs +1 -1
- package/src/services/email-imap.mjs +74 -44
- package/src/services/email-smtp.mjs +4 -3
- package/src/services/web-ui.mjs +44 -8
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nothumanallowed",
|
|
3
|
-
"version": "13.5.
|
|
3
|
+
"version": "13.5.118",
|
|
4
4
|
"description": "NotHumanAllowed — 38 AI agents, 80 tools, Studio (visual agentic workflows). Email, calendar, browser automation, screen capture, canvas, cron/heartbeat, Alexandria E2E messaging, GitHub, Notion, Slack, voice chat, free AI (Liara), 28 languages. Zero-dependency CLI.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
package/src/constants.mjs
CHANGED
|
@@ -5,7 +5,7 @@ import { fileURLToPath } from 'url';
|
|
|
5
5
|
const __filename = fileURLToPath(import.meta.url);
|
|
6
6
|
const __dirname = path.dirname(__filename);
|
|
7
7
|
|
|
8
|
-
export const VERSION = '13.5.
|
|
8
|
+
export const VERSION = '13.5.118';
|
|
9
9
|
export const BASE_URL = 'https://nothumanallowed.com/cli';
|
|
10
10
|
export const API_BASE = 'https://nothumanallowed.com/api/v1';
|
|
11
11
|
|
|
@@ -58,16 +58,17 @@ function notifyIdle(accountId, folder) {
|
|
|
58
58
|
}
|
|
59
59
|
|
|
60
60
|
function createImapClient(label, creds, accountId) {
|
|
61
|
-
const
|
|
61
|
+
const port = parseInt(creds.imap_port, 10) || 993;
|
|
62
|
+
const isSecure = port === 993 || port === 465;
|
|
62
63
|
const client = new ImapFlow({
|
|
63
64
|
host: creds.imap_host,
|
|
64
|
-
port
|
|
65
|
+
port,
|
|
65
66
|
secure: isSecure,
|
|
66
67
|
auth: { user: creds.username, pass: creds.password },
|
|
67
68
|
logger: false,
|
|
68
69
|
clientInfo: { name: 'NHA-Mail', version: '1.0.0' },
|
|
69
70
|
emitLogs: false,
|
|
70
|
-
|
|
71
|
+
tls: { rejectUnauthorized: false }, // always set — safe for self-signed certs too
|
|
71
72
|
});
|
|
72
73
|
client.on('error', (err) => {
|
|
73
74
|
console.error(`[email:imap] ${label} error:`, err.message);
|
|
@@ -147,36 +148,51 @@ function stripQuotedReplies(text) {
|
|
|
147
148
|
return text.split('\n').filter(l => !l.startsWith('>')).join('\n').trim();
|
|
148
149
|
}
|
|
149
150
|
|
|
150
|
-
export async function syncFolder(accountId, folderPath, fullResync) {
|
|
151
|
+
export async function syncFolder(accountId, folderPath, fullResync, limitMessages = 0) {
|
|
151
152
|
const client = await getImapClient(accountId);
|
|
152
153
|
const db = getDb();
|
|
153
154
|
const folderMeta = getFolder(accountId, folderPath);
|
|
155
|
+
|
|
156
|
+
// Open mailbox to get uidValidity BEFORE getMailboxLock
|
|
154
157
|
const lock = await client.getMailboxLock(folderPath);
|
|
155
158
|
|
|
156
159
|
try {
|
|
157
160
|
const mailbox = client.mailbox;
|
|
158
161
|
const currentUidValidity = Number(mailbox?.uidValidity ?? 0);
|
|
162
|
+
const totalMessages = Number(mailbox?.exists ?? 0);
|
|
159
163
|
const storedUidValidity = folderMeta?.uid_validity ?? null;
|
|
160
|
-
const
|
|
164
|
+
const isFirstSync = storedUidValidity === null || fullResync;
|
|
165
|
+
const lastUid = isFirstSync ? 0 : (folderMeta?.last_uid ?? 0);
|
|
161
166
|
const needsResync = fullResync || (storedUidValidity !== null && storedUidValidity !== currentUidValidity);
|
|
162
167
|
|
|
163
168
|
if (needsResync && folderMeta) {
|
|
164
|
-
// Clear existing messages for this folder on UID validity change
|
|
165
169
|
db.prepare('DELETE FROM email_message_labels WHERE message_id IN (SELECT id FROM email_messages WHERE account_id = ? AND imap_folder_path = ?)').run(accountId, folderPath);
|
|
166
170
|
db.prepare('DELETE FROM email_messages WHERE account_id = ? AND imap_folder_path = ?').run(accountId, folderPath);
|
|
167
171
|
}
|
|
168
172
|
|
|
169
173
|
const inboxLabel = getSystemLabel(accountId, 'inbox');
|
|
170
174
|
const sentLabel = getSystemLabel(accountId, 'sent');
|
|
171
|
-
const blockedLabel = getSystemLabel(accountId, 'spam');
|
|
172
175
|
|
|
173
176
|
let newLastUid = lastUid;
|
|
174
177
|
let synced = 0;
|
|
175
178
|
|
|
176
|
-
|
|
179
|
+
// On first sync: fetch only the most recent N messages by sequence number
|
|
180
|
+
// On incremental sync: fetch new UIDs since last known
|
|
181
|
+
let range;
|
|
182
|
+
if (isFirstSync && limitMessages > 0 && totalMessages > limitMessages) {
|
|
183
|
+
const seqStart = totalMessages - limitMessages + 1;
|
|
184
|
+
range = `${seqStart}:*`;
|
|
185
|
+
} else if (lastUid > 0) {
|
|
186
|
+
range = `${lastUid + 1}:*`;
|
|
187
|
+
} else {
|
|
188
|
+
range = '1:*';
|
|
189
|
+
}
|
|
177
190
|
|
|
191
|
+
// Phase 1: fetch headers only (fast — no body download)
|
|
192
|
+
const headerMap = new Map(); // uid → parsed header data
|
|
178
193
|
for await (const msg of client.fetch(range, {
|
|
179
194
|
uid: true, flags: true, envelope: true, internalDate: true, size: true,
|
|
195
|
+
bodyStructure: true,
|
|
180
196
|
}, { uid: true })) {
|
|
181
197
|
if (msg.uid <= lastUid) continue;
|
|
182
198
|
newLastUid = Math.max(newLastUid, msg.uid);
|
|
@@ -185,36 +201,48 @@ export async function syncFolder(accountId, folderPath, fullResync) {
|
|
|
185
201
|
const fromAddr = env.from?.[0]?.address ?? null;
|
|
186
202
|
const fromName = env.from?.[0]?.name ?? null;
|
|
187
203
|
|
|
188
|
-
|
|
189
|
-
if (fromAddr && isSenderBlocked(accountId, fromAddr)) {
|
|
190
|
-
synced++;
|
|
191
|
-
continue;
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
// Check if already synced
|
|
204
|
+
if (fromAddr && isSenderBlocked(accountId, fromAddr)) { synced++; continue; }
|
|
195
205
|
if (messageExists(accountId, folderPath, msg.uid)) { synced++; continue; }
|
|
196
206
|
|
|
197
|
-
|
|
207
|
+
headerMap.set(msg.uid, {
|
|
208
|
+
env, fromAddr, fromName,
|
|
209
|
+
internalDate: (msg.internalDate || new Date()).toISOString(),
|
|
210
|
+
size: msg.size || 0,
|
|
211
|
+
flags: msg.flags,
|
|
212
|
+
bodyStructure: msg.bodyStructure,
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Phase 2: for each new message, fetch body (individually, with error isolation)
|
|
217
|
+
const uidsToFetch = [...headerMap.keys()];
|
|
218
|
+
for (const uid of uidsToFetch) {
|
|
219
|
+
const hdr = headerMap.get(uid);
|
|
220
|
+
const env = hdr.env;
|
|
221
|
+
const fromAddr = hdr.fromAddr;
|
|
222
|
+
const fromName = hdr.fromName;
|
|
223
|
+
|
|
198
224
|
let bodyText = null, bodyHtml = null, bodyPreview = '', attachments = [];
|
|
199
225
|
let inReplyTo = null, references = [], msgId = null;
|
|
200
226
|
|
|
201
227
|
try {
|
|
202
|
-
const dl = await client.download(String(
|
|
228
|
+
const dl = await client.download(String(uid), undefined, { uid: true });
|
|
203
229
|
if (dl) {
|
|
204
230
|
const chunks = [];
|
|
205
231
|
let totalSize = 0;
|
|
206
232
|
for await (const chunk of dl.content) {
|
|
207
233
|
totalSize += chunk.length;
|
|
208
|
-
if (totalSize >
|
|
234
|
+
if (totalSize > 10 * 1024 * 1024) break; // 10MB cap per message
|
|
209
235
|
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
210
236
|
}
|
|
211
237
|
const raw = Buffer.concat(chunks);
|
|
212
|
-
const parsed = await simpleParser(raw);
|
|
238
|
+
const parsed = await simpleParser(raw, { skipHtmlToText: false });
|
|
213
239
|
msgId = parsed.messageId || null;
|
|
214
|
-
inReplyTo = typeof parsed.inReplyTo === 'string' ? parsed.inReplyTo
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
240
|
+
inReplyTo = typeof parsed.inReplyTo === 'string' ? parsed.inReplyTo
|
|
241
|
+
: (Array.isArray(parsed.inReplyTo) ? parsed.inReplyTo[0] : null);
|
|
242
|
+
references = Array.isArray(parsed.references) ? parsed.references.slice(0, 20)
|
|
243
|
+
: (parsed.references ? [parsed.references] : []);
|
|
244
|
+
bodyText = (typeof parsed.text === 'string' ? parsed.text : null)?.replace(/\x00/g, '') || null;
|
|
245
|
+
bodyHtml = (typeof parsed.html === 'string' ? parsed.html : null)?.replace(/\x00/g, '') || null;
|
|
218
246
|
bodyPreview = (bodyText || '').replace(/\s+/g, ' ').trim().slice(0, 255);
|
|
219
247
|
|
|
220
248
|
if (parsed.attachments) {
|
|
@@ -225,30 +253,31 @@ export async function syncFolder(accountId, folderPath, fullResync) {
|
|
|
225
253
|
size_bytes: att.size || 0,
|
|
226
254
|
part_id: att.partId || '',
|
|
227
255
|
content_id: att.contentId || null,
|
|
228
|
-
content: att.size < 5 * 1024 * 1024 ? att.content : null,
|
|
256
|
+
content: (att.size || 0) < 5 * 1024 * 1024 ? att.content : null,
|
|
229
257
|
});
|
|
230
258
|
}
|
|
231
259
|
}
|
|
232
260
|
}
|
|
233
261
|
} catch (err) {
|
|
234
|
-
console.warn(`[email:imap] Failed to fetch body uid=${
|
|
262
|
+
console.warn(`[email:imap] Failed to fetch body uid=${uid}:`, err.message);
|
|
263
|
+
// Continue with headers-only for this message
|
|
235
264
|
}
|
|
236
265
|
|
|
237
266
|
const toAddresses = (env.to || []).map(a => ({ address: a.address, name: a.name }));
|
|
238
267
|
const ccAddresses = (env.cc || []).map(a => ({ address: a.address, name: a.name }));
|
|
239
268
|
const bccAddresses = (env.bcc || []).map(a => ({ address: a.address, name: a.name }));
|
|
240
269
|
const subject = env.subject || '';
|
|
241
|
-
const
|
|
242
|
-
const
|
|
243
|
-
const hash = contentHash(msgId || '', fromAddr || '', internalDate);
|
|
270
|
+
const tid = threadId(msgId, inReplyTo, references, fromAddr, hdr.internalDate, subject);
|
|
271
|
+
const hash = contentHash(msgId || '', fromAddr || '', hdr.internalDate);
|
|
244
272
|
|
|
245
|
-
const folderRec = upsertFolder(accountId, folderPath, folderPath,
|
|
273
|
+
const folderRec = upsertFolder(accountId, folderPath, folderPath,
|
|
274
|
+
folderMeta?.folder_type || 'custom', currentUidValidity, newLastUid);
|
|
246
275
|
|
|
247
276
|
const msgDbId = insertMessage({
|
|
248
277
|
account_id: accountId,
|
|
249
278
|
folder_id: folderRec,
|
|
250
279
|
imap_folder_path: folderPath,
|
|
251
|
-
uid
|
|
280
|
+
uid,
|
|
252
281
|
message_id: msgId,
|
|
253
282
|
in_reply_to: inReplyTo,
|
|
254
283
|
references_list: references,
|
|
@@ -263,42 +292,41 @@ export async function syncFolder(accountId, folderPath, fullResync) {
|
|
|
263
292
|
body_html: bodyHtml,
|
|
264
293
|
body_preview: bodyPreview,
|
|
265
294
|
body_reply_only: stripQuotedReplies(bodyText),
|
|
266
|
-
size_bytes:
|
|
295
|
+
size_bytes: hdr.size,
|
|
267
296
|
has_attachments: attachments.length > 0,
|
|
268
297
|
content_hash: hash,
|
|
269
|
-
internal_date: internalDate,
|
|
298
|
+
internal_date: hdr.internalDate,
|
|
270
299
|
source: 'imap',
|
|
271
300
|
});
|
|
272
301
|
|
|
273
302
|
if (attachments.length > 0) insertAttachments(msgDbId, attachments);
|
|
274
303
|
|
|
275
|
-
// Apply archiving rules (first match wins, removes from inbox)
|
|
276
304
|
const archived = applyArchivingRules(accountId, msgDbId, fromAddr, subject);
|
|
277
305
|
if (!archived) {
|
|
278
|
-
|
|
279
|
-
const
|
|
280
|
-
const targetLabel = isSent ? sentLabel : inboxLabel;
|
|
306
|
+
const isSentFolder = folderPath.toLowerCase().includes('sent') || folderPath.toLowerCase().includes('inviati');
|
|
307
|
+
const targetLabel = isSentFolder ? sentLabel : inboxLabel;
|
|
281
308
|
if (targetLabel) addMessageToLabel(msgDbId, targetLabel.id);
|
|
282
309
|
}
|
|
283
310
|
|
|
284
311
|
synced++;
|
|
285
312
|
}
|
|
286
313
|
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
folderPath, folderMeta?.folder_type || 'custom',
|
|
290
|
-
currentUidValidity, newLastUid);
|
|
314
|
+
upsertFolder(accountId, folderPath, folderPath,
|
|
315
|
+
folderMeta?.folder_type || 'custom', currentUidValidity, newLastUid);
|
|
291
316
|
|
|
292
|
-
|
|
317
|
+
updateLabelCounts(accountId);
|
|
318
|
+
return { synced, lastUid: newLastUid, total: totalMessages };
|
|
293
319
|
} finally {
|
|
294
320
|
lock.release();
|
|
295
321
|
}
|
|
296
322
|
}
|
|
297
323
|
|
|
298
|
-
|
|
324
|
+
// First sync: cap to last 200 messages per folder to avoid blocking for minutes
|
|
325
|
+
const FIRST_SYNC_LIMIT = 200;
|
|
326
|
+
|
|
327
|
+
export async function syncAccount(accountId, opts = {}) {
|
|
299
328
|
setSyncStatus(accountId, 'syncing', null);
|
|
300
329
|
try {
|
|
301
|
-
// List folders from IMAP and sync INBOX + Sent
|
|
302
330
|
const folders = await listImapFolders(accountId);
|
|
303
331
|
const priority = ['inbox', 'sent'];
|
|
304
332
|
const toSync = [
|
|
@@ -309,8 +337,10 @@ export async function syncAccount(accountId) {
|
|
|
309
337
|
let totalSynced = 0;
|
|
310
338
|
for (const f of toSync) {
|
|
311
339
|
try {
|
|
312
|
-
const
|
|
340
|
+
const limit = opts.full ? 0 : FIRST_SYNC_LIMIT;
|
|
341
|
+
const result = await syncFolder(accountId, f.path, false, limit);
|
|
313
342
|
totalSynced += result.synced;
|
|
343
|
+
console.log(`[email:sync] ${f.path}: ${result.synced} new messages (total on server: ${result.total})`);
|
|
314
344
|
} catch (err) {
|
|
315
345
|
console.warn(`[email:imap] Sync folder ${f.path} failed:`, err.message);
|
|
316
346
|
}
|
|
@@ -19,16 +19,17 @@ function threadId(messageId) {
|
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
function getTransporter(creds) {
|
|
22
|
-
const
|
|
22
|
+
const port = parseInt(creds.smtp_port, 10) || 587;
|
|
23
|
+
const isSecure = port === 465;
|
|
23
24
|
return createTransport({
|
|
24
25
|
host: creds.smtp_host,
|
|
25
|
-
port
|
|
26
|
+
port,
|
|
26
27
|
secure: isSecure,
|
|
27
28
|
auth: { user: creds.username, pass: creds.password },
|
|
28
29
|
connectionTimeout: 15000,
|
|
29
30
|
greetingTimeout: 10000,
|
|
30
31
|
socketTimeout: 20000,
|
|
31
|
-
tls: { rejectUnauthorized:
|
|
32
|
+
tls: { rejectUnauthorized: false },
|
|
32
33
|
});
|
|
33
34
|
}
|
|
34
35
|
|
package/src/services/web-ui.mjs
CHANGED
|
@@ -3143,12 +3143,12 @@ function renderImapAccountsSettings() {
|
|
|
3143
3143
|
imapField('imapDisplayName','Display Name','e.g. Work Email') +
|
|
3144
3144
|
imapField('imapEmail','Email Address','user@example.com') +
|
|
3145
3145
|
imapField('imapFromName','From Name','e.g. John Smith') +
|
|
3146
|
-
imapField('imapImapHost','IMAP
|
|
3147
|
-
imapField('imapImapPort','IMAP Port','993') +
|
|
3148
|
-
imapField('imapSmtpHost','SMTP
|
|
3149
|
-
imapField('imapSmtpPort','SMTP Port','587') +
|
|
3146
|
+
imapField('imapImapHost','IMAP Server','e.g. imap.gmail.com') +
|
|
3147
|
+
imapField('imapImapPort','IMAP Port','993 (TLS) or 143') +
|
|
3148
|
+
imapField('imapSmtpHost','SMTP Server','e.g. smtp.gmail.com') +
|
|
3149
|
+
imapField('imapSmtpPort','SMTP Port','587 (STARTTLS) or 465 (SSL)') +
|
|
3150
3150
|
imapField('imapUsername','Username','e.g. user@example.com') +
|
|
3151
|
-
|
|
3151
|
+
imapFieldPassword() +
|
|
3152
3152
|
'<div style="display:flex;gap:8px;margin-top:12px">' +
|
|
3153
3153
|
'<button onclick="saveImapAccount()" style="background:var(--green3);color:var(--bg);padding:7px 18px;border-radius:var(--r);font-weight:700;font-size:12px;cursor:pointer;border:none">Save</button>' +
|
|
3154
3154
|
'<button onclick="document.getElementById(\\x27imapAccountForm\\x27).style.display=\\x27none\\x27" style="background:var(--bg);color:var(--dim);padding:7px 14px;border-radius:var(--r);font-size:12px;cursor:pointer;border:1px solid var(--border)">Cancel</button>' +
|
|
@@ -3158,13 +3158,31 @@ function renderImapAccountsSettings() {
|
|
|
3158
3158
|
'</div>';
|
|
3159
3159
|
}
|
|
3160
3160
|
|
|
3161
|
-
function imapField(id, label, placeholder
|
|
3161
|
+
function imapField(id, label, placeholder) {
|
|
3162
3162
|
return '<div style="margin-bottom:8px">' +
|
|
3163
3163
|
'<label style="display:block;font-size:10px;color:var(--dim);margin-bottom:3px">' + label + '</label>' +
|
|
3164
|
-
'<input id="' + id + '" type="
|
|
3164
|
+
'<input id="' + id + '" type="text" placeholder="' + placeholder + '" style="width:100%;padding:7px 10px;font-size:12px;background:var(--bg);color:var(--fg);border:1px solid var(--border2);border-radius:6px;box-sizing:border-box">' +
|
|
3165
3165
|
'</div>';
|
|
3166
3166
|
}
|
|
3167
3167
|
|
|
3168
|
+
function imapFieldPassword() {
|
|
3169
|
+
return '<div style="margin-bottom:8px">' +
|
|
3170
|
+
'<label style="display:block;font-size:10px;color:var(--dim);margin-bottom:3px">Password <span id="imapPwdPlaceholderNote" style="color:var(--amber,#F59E0B)">(leave empty to keep existing)</span></label>' +
|
|
3171
|
+
'<div style="display:flex;gap:6px;align-items:center">' +
|
|
3172
|
+
'<input id="imapPassword" type="password" placeholder="App password or IMAP password" style="flex:1;padding:7px 10px;font-size:12px;background:var(--bg);color:var(--fg);border:1px solid var(--border2);border-radius:6px">' +
|
|
3173
|
+
'<button type="button" onclick="imapTogglePwd()" style="padding:6px 10px;font-size:11px;background:var(--bg);color:var(--dim);border:1px solid var(--border);border-radius:6px;cursor:pointer;white-space:nowrap" id="imapPwdToggle">Show</button>' +
|
|
3174
|
+
'</div>' +
|
|
3175
|
+
'</div>';
|
|
3176
|
+
}
|
|
3177
|
+
|
|
3178
|
+
function imapTogglePwd() {
|
|
3179
|
+
var inp = document.getElementById('imapPassword');
|
|
3180
|
+
var btn = document.getElementById('imapPwdToggle');
|
|
3181
|
+
if (!inp) return;
|
|
3182
|
+
if (inp.type === 'password') { inp.type = 'text'; if (btn) btn.textContent = 'Hide'; }
|
|
3183
|
+
else { inp.type = 'password'; if (btn) btn.textContent = 'Show'; }
|
|
3184
|
+
}
|
|
3185
|
+
|
|
3168
3186
|
function loadImapAccounts() {
|
|
3169
3187
|
apiGet('/api/imap/accounts').then(function(r) {
|
|
3170
3188
|
var el = document.getElementById('imapAccountsList');
|
|
@@ -3212,6 +3230,14 @@ function showAddImapAccount() {
|
|
|
3212
3230
|
}
|
|
3213
3231
|
var status = document.getElementById('imapFormStatus');
|
|
3214
3232
|
if (status) status.textContent = '';
|
|
3233
|
+
// New account: hide "leave empty" note, require password
|
|
3234
|
+
var note = document.getElementById('imapPwdPlaceholderNote');
|
|
3235
|
+
if (note) note.style.display = 'none';
|
|
3236
|
+
// Reset show/hide toggle
|
|
3237
|
+
var pwdInp = document.getElementById('imapPassword');
|
|
3238
|
+
var pwdBtn = document.getElementById('imapPwdToggle');
|
|
3239
|
+
if (pwdInp) { pwdInp.type = 'password'; pwdInp.placeholder = 'App password or IMAP password'; }
|
|
3240
|
+
if (pwdBtn) pwdBtn.textContent = 'Show';
|
|
3215
3241
|
form.style.display = 'block';
|
|
3216
3242
|
form.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
|
|
3217
3243
|
}
|
|
@@ -3231,7 +3257,17 @@ function editImapAccount(id) {
|
|
|
3231
3257
|
document.getElementById('imapSmtpPort').value = a.smtp_port || 587;
|
|
3232
3258
|
document.getElementById('imapUsername').value = a.username || '';
|
|
3233
3259
|
document.getElementById('imapPassword').value = '';
|
|
3234
|
-
|
|
3260
|
+
// Show "leave empty to keep" note and reset toggle
|
|
3261
|
+
var note = document.getElementById('imapPwdPlaceholderNote');
|
|
3262
|
+
if (note) note.style.display = 'inline';
|
|
3263
|
+
var pwdInp = document.getElementById('imapPassword');
|
|
3264
|
+
var pwdBtn = document.getElementById('imapPwdToggle');
|
|
3265
|
+
if (pwdInp) { pwdInp.type = 'password'; pwdInp.placeholder = 'Leave empty to keep existing'; }
|
|
3266
|
+
if (pwdBtn) pwdBtn.textContent = 'Show';
|
|
3267
|
+
var form2 = document.getElementById('imapAccountForm');
|
|
3268
|
+
var status2 = document.getElementById('imapFormStatus');
|
|
3269
|
+
if (status2) status2.textContent = '';
|
|
3270
|
+
if (form2) { form2.style.display = 'block'; form2.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); }
|
|
3235
3271
|
});
|
|
3236
3272
|
}
|
|
3237
3273
|
|