Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ Notable changes to **pixelpets**. All art and sound are original/procedural (no
- **A treat handed over during a meeting gets eaten.** Parking the pet in its corner aims the same walk the trip to the fish uses, one step earlier in the frame, so it re-claimed that walk the moment the trip expired: the cat set off, was marched home just short of its food, set off again, and paced between corner and fish for as long as the treat existed - which is forever, since only eating clears it. Work mode is there to stop the pet wandering off on its own, not to veto something you clicked.

### Mail
- **A wrong kind of password is called out when you save it.** Settings showed "saved" for any password of eight characters or more. With a Gmail address that was misleading: Gmail only accepts a 16-letter app password here, so anyone who pasted their normal Google password got a tick, and then alerts that never came, with nothing to say why. Settings now says the saved password is not 16 characters, what that usually means, and where to make an app password. Only the length is checked, and the wording allows for work and school accounts, which an admin can set up differently.
- **The pet says who it is from.** "You have 3 new emails" still makes you go and look, which is the interruption it was meant to save you. It now reads *"Alice: Budget review"*. The envelope fetch is wrapped separately from the count, so a server that refuses it still gets you the old count-only line rather than losing the alert.
- **VIP senders.** Name the people who should reach you even while Focus Guard is holding everything else back. Matching is a case-insensitive substring of the From address - no pattern syntax to get wrong - so `@acme.com` covers a company and `boss@acme.com` one person.

Expand Down
18 changes: 15 additions & 3 deletions src/mail.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,21 @@ function normalizePassword(plain) {
// stale or undecryptable email.cred used to show a reassuring "saved" tick in
// Settings while every poll bailed out for want of a password. The length lets the
// UI call out a truncated app-password instead of pretending it is fine.
function passwordInfo() {
function passwordInfo(cfg) {
const pw = readPassword();
return { has: pw.length > 0, len: pw.length };
const e = (cfg && cfg.email) || {};
return { has: pw.length > 0, len: pw.length, advice: passwordAdvice(pw.length, e.user, e.host) };
}

// Google issues an app password as exactly sixteen letters, and setPassword has
// already taken the spaces out. Any other length saved against a Gmail account
// is almost always the account's own password, which Gmail refuses over IMAP,
// so the poll fails quietly and the alerts simply never come. Only the length
// is looked at: the password itself is never compared, logged or sent anywhere.
const GMAIL_APP_PASSWORD_LEN = 16;
function passwordAdvice(len, email, host) {
if (!len) return null;
return imapHostFor(email, host) === 'imap.gmail.com' && len !== GMAIL_APP_PASSWORD_LEN ? 'gmail-length' : null;
}
function hasPassword() { return passwordInfo().has; }
function setPassword(plain) {
Expand Down Expand Up @@ -172,4 +184,4 @@ function test(cfg, plainOverride) {
function init(notify_, getCfg_) { notifyFn = notify_; getCfg = getCfg_; }
function stop() { if (timer) { clearInterval(timer); timer = null; } }

module.exports = { init, sync, test, setPassword, hasPassword, passwordInfo, stop, imapHostFor, normalizePassword, isVip, describe };
module.exports = { init, sync, test, setPassword, hasPassword, passwordInfo, passwordAdvice, GMAIL_APP_PASSWORD_LEN, stop, imapHostFor, normalizePassword, isVip, describe };
2 changes: 1 addition & 1 deletion src/main/settings-ipc.js
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ function registerSettingsIpc(d) {
d.sendAction(id);
});

handleSecure('email:passwordInfo', () => d.mail.passwordInfo());
handleSecure('email:passwordInfo', () => d.mail.passwordInfo(getCfg()));
handleSecure('email:setPassword', (_e, pw) => d.mail.setPassword(pw));
handleSecure('email:test', (_e, pw) => d.mail.test(getCfg(), pw && String(pw).length ? String(pw) : null));
handleSecure('calendar:test', () => d.cal.test(getCfg()));
Expand Down
19 changes: 12 additions & 7 deletions src/settings-renderer.js
Original file line number Diff line number Diff line change
Expand Up @@ -296,15 +296,20 @@ $('emailOn').addEventListener('change', async () => {
: 'Alerts are on. Hit Test to check the connection.';
});
const APP_PASSWORD_MIN = 8; // shorter than any provider issues: a sign of a truncated paste
// What may be wrong with the saved password, as a sentence, or '' when nothing is.
// Worded as likely, not certain: a work or school Google account can be set up
// in ways this cannot see.
const GMAIL_PASSWORD_HELP = 'Gmail usually needs a 16-letter app password, and the one saved is a different length. If it is your normal Google password, Gmail will refuse it. You can make an app password at myaccount.google.com/apppasswords once 2-Step Verification is on. On a work or school account, your admin may have to allow it.';
async function refreshEmailPassState() {
try {
const info = await window.settings.emailPasswordInfo();
const el = $('emailPassState');
if (!info || !info.has) { el.textContent = ''; return; }
el.textContent = info.len < APP_PASSWORD_MIN
? `\u00b7 saved, but only ${info.len} characters; re-enter it`
: '\u00b7 saved \u2713';
} catch (e) { /* ignore */ }
if (!info || !info.has) { el.textContent = ''; return ''; }
if (info.len < APP_PASSWORD_MIN) { el.textContent = `\u00b7 saved, but only ${info.len} characters; re-enter it`; return ''; }
if (info.advice === 'gmail-length') { el.textContent = `\u00b7 saved, but ${info.len} characters, not 16`; return GMAIL_PASSWORD_HELP; }
el.textContent = '\u00b7 saved \u2713';
return '';
} catch (e) { return ''; }
}
// Save the app-password when the field is done, never mid-keystroke. The old
// 600ms auto-save stored whatever had been typed so far and then blanked the box,
Expand All @@ -317,10 +322,10 @@ function emailPassSave() {
return (async () => {
const r = await window.settings.emailSetPassword(pw);
$('emailPass').value = '';
await refreshEmailPassState();
const problem = await refreshEmailPassState();
$('emailStatus').textContent = (r && r.ok === false)
? ('Could not save the password: ' + (r.error || 'unknown error'))
: `Password saved (${(r && r.len) | 0} characters, encrypted).`;
: (problem || `Password saved (${(r && r.len) | 0} characters, encrypted).`);
})();
}
$('emailPass').addEventListener('change', emailPassSave);
Expand Down
26 changes: 26 additions & 0 deletions tests/mail.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -84,3 +84,29 @@ test('no envelope falls back to the plain count', () => {
assert.strictEqual(mail.describe(2, null), 'You have {count} new emails.');
assert.strictEqual(mail.describe(2, { name: '', address: '', subject: '' }), 'You have {count} new emails.');
});

test('a Gmail account with a password that is not sixteen letters is called out', () => {
const a = mail.passwordAdvice;
assert.strictEqual(a(16, 'me@gmail.com', 'imap.gmail.com'), null, 'a real app password');
assert.strictEqual(a(19, 'me@gmail.com', 'imap.gmail.com'), 'gmail-length', 'most likely the account password');
assert.strictEqual(a(12, 'me@googlemail.com', ''), 'gmail-length', 'the host is inferred from the address');
assert.strictEqual(a(19, 'me@gmail.com', 'www.gmail.com'), 'gmail-length', 'a web address still means Gmail');
// Other providers issue other lengths, and a self-hosted server can use anything.
assert.strictEqual(a(19, 'me@outlook.com', ''), null);
assert.strictEqual(a(40, 'me@example.org', 'mail.example.org'), null);
assert.strictEqual(a(0, 'me@gmail.com', 'imap.gmail.com'), null, 'nothing saved is not a wrong password');
assert.strictEqual(mail.GMAIL_APP_PASSWORD_LEN, 16);
});

test('Settings turns that advice into a sentence, and asks about the account it is showing', () => {
const fs = require('node:fs');
const path = require('node:path');
const src = (f) => fs.readFileSync(path.join(__dirname, '..', 'src', f), 'utf8');
const ui = src('settings-renderer.js');
assert.match(ui, /info\.advice === 'gmail-length'/);
assert.match(ui, /myaccount\.google\.com\/apppasswords/);
// The advice depends on the address and server, so main must be handed the config.
assert.match(src(path.join('main', 'settings-ipc.js')), /passwordInfo\(getCfg\(\)\)/);
// The sentence replaces 'Password saved' so it is not buried under it.
assert.match(ui, /problem \|\| `Password saved/);
});
Loading