@bobfrankston/mailx 1.0.173 → 1.0.175
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/bin/mailx.js +3 -0
- package/client/.msger-window.json +1 -0
- package/client/app.js +8 -2
- package/client/components/message-viewer.js +6 -1
- package/client/compose/compose.js +10 -1
- package/client/compose/editor.js +3 -0
- package/package.json +3 -3
- package/packages/mailx-imap/index.d.ts +2 -0
- package/packages/mailx-imap/index.js +68 -2
- package/sending/gmail/20260407_210213-queued.eml +58 -0
package/bin/mailx.js
CHANGED
|
@@ -740,10 +740,13 @@ async function main() {
|
|
|
740
740
|
console.log(`${reason} — shutting down`);
|
|
741
741
|
imapManager.stopPeriodicSync();
|
|
742
742
|
imapManager.stopOutboxWorker();
|
|
743
|
+
// 3s hard timeout — don't hang on broken IMAP connections
|
|
744
|
+
const forceExit = setTimeout(() => { console.log("Forced exit"); process.exit(0); }, 3000);
|
|
743
745
|
try {
|
|
744
746
|
await imapManager.shutdown();
|
|
745
747
|
}
|
|
746
748
|
catch { /* proceed */ }
|
|
749
|
+
clearTimeout(forceExit);
|
|
747
750
|
db.close();
|
|
748
751
|
process.exit(0);
|
|
749
752
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"height":1047,"width":1844,"x":664,"y":330}
|
package/client/app.js
CHANGED
|
@@ -286,8 +286,14 @@ async function openCompose(mode) {
|
|
|
286
286
|
}
|
|
287
287
|
// Store init data for compose window to pick up
|
|
288
288
|
sessionStorage.setItem("composeInit", JSON.stringify(init));
|
|
289
|
-
//
|
|
290
|
-
|
|
289
|
+
// IPC mode: navigate in same window (popups don't have custom protocol)
|
|
290
|
+
// HTTP mode: open as popup window
|
|
291
|
+
if (typeof mailxapi !== "undefined") {
|
|
292
|
+
window.location.href = "compose/compose.html";
|
|
293
|
+
}
|
|
294
|
+
else {
|
|
295
|
+
window.open("compose/compose.html", "_blank", "width=800,height=600,menubar=no,toolbar=no,status=no");
|
|
296
|
+
}
|
|
291
297
|
}
|
|
292
298
|
function quoteBody(msg) {
|
|
293
299
|
const date = new Date(msg.date).toLocaleString();
|
|
@@ -135,7 +135,12 @@ export async function showMessage(accountId, uid, folderId, specialUse, isRetry
|
|
|
135
135
|
draftFolderId: msg.folderId,
|
|
136
136
|
};
|
|
137
137
|
sessionStorage.setItem("composeInit", JSON.stringify(init));
|
|
138
|
-
window.
|
|
138
|
+
if (typeof window.mailxapi !== "undefined") {
|
|
139
|
+
window.location.href = "compose/compose.html";
|
|
140
|
+
}
|
|
141
|
+
else {
|
|
142
|
+
window.open("compose/compose.html", "_blank", "width=800,height=600,menubar=no,toolbar=no,status=no");
|
|
143
|
+
}
|
|
139
144
|
};
|
|
140
145
|
}
|
|
141
146
|
else {
|
|
@@ -5,6 +5,15 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import { createEditor } from "./editor.js";
|
|
7
7
|
import { getVersion, getSettings, getAccounts, searchContacts, sendMessage, saveDraft as apiSaveDraft, deleteDraft } from "../lib/api-client.js";
|
|
8
|
+
/** Close compose — navigate back in IPC mode, window.close() in HTTP mode */
|
|
9
|
+
function closeCompose() {
|
|
10
|
+
if (typeof window.mailxapi !== "undefined") {
|
|
11
|
+
window.location.href = "../index.html";
|
|
12
|
+
}
|
|
13
|
+
else {
|
|
14
|
+
closeCompose();
|
|
15
|
+
}
|
|
16
|
+
}
|
|
8
17
|
// ── Load editor scripts dynamically ──
|
|
9
18
|
function loadScript(src) {
|
|
10
19
|
return new Promise((resolve, reject) => {
|
|
@@ -349,7 +358,7 @@ document.getElementById("btn-send")?.addEventListener("click", async () => {
|
|
|
349
358
|
if (draftUid) {
|
|
350
359
|
deleteDraft(getFromAccountId(), draftUid).catch(() => { });
|
|
351
360
|
}
|
|
352
|
-
|
|
361
|
+
closeCompose();
|
|
353
362
|
}
|
|
354
363
|
catch (e) {
|
|
355
364
|
btn.disabled = false;
|
package/client/compose/editor.js
CHANGED
|
@@ -8,9 +8,12 @@ function createQuillEditor(container) {
|
|
|
8
8
|
placeholder: "Write your message...",
|
|
9
9
|
modules: {
|
|
10
10
|
toolbar: [
|
|
11
|
+
[{ font: [] }, { size: ["small", false, "large", "huge"] }],
|
|
11
12
|
[{ header: [1, 2, 3, false] }],
|
|
12
13
|
["bold", "italic", "underline", "strike"],
|
|
14
|
+
[{ color: [] }, { background: [] }],
|
|
13
15
|
[{ list: "ordered" }, { list: "bullet" }],
|
|
16
|
+
[{ align: [] }],
|
|
14
17
|
["blockquote", "link", "image"],
|
|
15
18
|
["clean"]
|
|
16
19
|
]
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bobfrankston/mailx",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.175",
|
|
4
4
|
"description": "Local-first email client with IMAP sync and standalone native app",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "bin/mailx.js",
|
|
@@ -20,11 +20,11 @@
|
|
|
20
20
|
"postinstall": "node bin/postinstall.js"
|
|
21
21
|
},
|
|
22
22
|
"dependencies": {
|
|
23
|
-
"@bobfrankston/iflow-direct": "^0.1.
|
|
23
|
+
"@bobfrankston/iflow-direct": "^0.1.5",
|
|
24
24
|
"@bobfrankston/iflow-node": "^0.1.1",
|
|
25
25
|
"@bobfrankston/miscinfo": "^1.0.7",
|
|
26
26
|
"@bobfrankston/oauthsupport": "^1.0.20",
|
|
27
|
-
"@bobfrankston/msger": "^0.1.
|
|
27
|
+
"@bobfrankston/msger": "^0.1.224",
|
|
28
28
|
"@capacitor/android": "^8.3.0",
|
|
29
29
|
"@capacitor/cli": "^8.3.0",
|
|
30
30
|
"@capacitor/core": "^8.3.0",
|
|
@@ -184,6 +184,8 @@ export declare class ImapManager extends EventEmitter {
|
|
|
184
184
|
queueOutgoing(accountId: string, rawMessage: string | Buffer): Promise<void>;
|
|
185
185
|
/** Process local file queue — move to IMAP Outbox when server is reachable */
|
|
186
186
|
private processLocalQueue;
|
|
187
|
+
/** Send a raw RFC 2822 message via SMTP for a given account */
|
|
188
|
+
private sendRawViaSMTP;
|
|
187
189
|
/** Process Outbox — send pending messages with flag-based interlock */
|
|
188
190
|
processOutbox(accountId: string): Promise<void>;
|
|
189
191
|
/** Start background Outbox worker — runs immediately then every 10 seconds */
|
|
@@ -291,8 +291,15 @@ export class ImapManager extends EventEmitter {
|
|
|
291
291
|
const client = this.opsClients.get(accountId);
|
|
292
292
|
this.opsClients.delete(accountId);
|
|
293
293
|
if (client) {
|
|
294
|
+
// Force-close: don't wait for LOGOUT on a possibly dead socket
|
|
294
295
|
try {
|
|
295
|
-
|
|
296
|
+
const timeout = new Promise(r => setTimeout(r, 2000));
|
|
297
|
+
await Promise.race([(client._realLogout || client.logout)(), timeout]);
|
|
298
|
+
}
|
|
299
|
+
catch { /* */ }
|
|
300
|
+
// Destroy underlying socket if still open
|
|
301
|
+
try {
|
|
302
|
+
client.destroy?.();
|
|
296
303
|
}
|
|
297
304
|
catch { /* */ }
|
|
298
305
|
console.log(` [conn] ${accountId}: disconnected`);
|
|
@@ -1674,6 +1681,23 @@ export class ImapManager extends EventEmitter {
|
|
|
1674
1681
|
const files = fs.readdirSync(localQueue).filter(f => f.endsWith(".ltr"));
|
|
1675
1682
|
if (files.length === 0)
|
|
1676
1683
|
return;
|
|
1684
|
+
// Gmail/API accounts: send directly via SMTP from local queue (no IMAP outbox)
|
|
1685
|
+
if (this.isGmailAccount(accountId)) {
|
|
1686
|
+
for (const file of files) {
|
|
1687
|
+
const filePath = path.join(localQueue, file);
|
|
1688
|
+
const raw = fs.readFileSync(filePath, "utf-8");
|
|
1689
|
+
try {
|
|
1690
|
+
await this.sendRawViaSMTP(accountId, raw);
|
|
1691
|
+
fs.unlinkSync(filePath);
|
|
1692
|
+
console.log(` [outbox] Sent local ${file} via SMTP`);
|
|
1693
|
+
}
|
|
1694
|
+
catch (e) {
|
|
1695
|
+
console.error(` [outbox] Send failed for ${file}: ${e.message}`);
|
|
1696
|
+
}
|
|
1697
|
+
}
|
|
1698
|
+
return;
|
|
1699
|
+
}
|
|
1700
|
+
// IMAP accounts: append to IMAP Outbox for multi-machine interlock
|
|
1677
1701
|
try {
|
|
1678
1702
|
const outboxPath = await this.ensureOutbox(accountId);
|
|
1679
1703
|
const client = await this.createClientWithLimit(accountId);
|
|
@@ -1697,6 +1721,48 @@ export class ImapManager extends EventEmitter {
|
|
|
1697
1721
|
// IMAP still unreachable — leave files for next attempt
|
|
1698
1722
|
}
|
|
1699
1723
|
}
|
|
1724
|
+
/** Send a raw RFC 2822 message via SMTP for a given account */
|
|
1725
|
+
async sendRawViaSMTP(accountId, raw) {
|
|
1726
|
+
const settings = loadSettings();
|
|
1727
|
+
const account = settings.accounts.find(a => a.id === accountId);
|
|
1728
|
+
if (!account?.smtp)
|
|
1729
|
+
throw new Error(`No SMTP config for ${accountId}`);
|
|
1730
|
+
let smtpAuth;
|
|
1731
|
+
if (account.smtp.auth === "password") {
|
|
1732
|
+
smtpAuth = { user: account.smtp.user, pass: account.smtp.password };
|
|
1733
|
+
}
|
|
1734
|
+
else if (account.smtp.auth === "oauth2") {
|
|
1735
|
+
const accessToken = await this.getOAuthToken(accountId);
|
|
1736
|
+
if (!accessToken)
|
|
1737
|
+
throw new Error("OAuth token not available");
|
|
1738
|
+
smtpAuth = { type: "OAuth2", user: account.smtp.user, accessToken };
|
|
1739
|
+
}
|
|
1740
|
+
const { createTransport } = await import("nodemailer");
|
|
1741
|
+
const transport = createTransport({
|
|
1742
|
+
host: account.smtp.host,
|
|
1743
|
+
port: account.smtp.port,
|
|
1744
|
+
secure: account.smtp.port === 465,
|
|
1745
|
+
auth: smtpAuth,
|
|
1746
|
+
tls: { rejectUnauthorized: false },
|
|
1747
|
+
});
|
|
1748
|
+
const parseAddrs = (s) => s.match(/[\w.+-]+@[\w.-]+/g) || [];
|
|
1749
|
+
const toMatch = raw.match(/^To:\s*(.+)$/mi);
|
|
1750
|
+
const ccMatch = raw.match(/^Cc:\s*(.+)$/mi);
|
|
1751
|
+
const bccMatch = raw.match(/^Bcc:\s*(.+)$/mi);
|
|
1752
|
+
const fromMatch = raw.match(/^From:\s*(.+)$/mi);
|
|
1753
|
+
const recipients = [
|
|
1754
|
+
...(toMatch ? parseAddrs(toMatch[1]) : []),
|
|
1755
|
+
...(ccMatch ? parseAddrs(ccMatch[1]) : []),
|
|
1756
|
+
...(bccMatch ? parseAddrs(bccMatch[1]) : []),
|
|
1757
|
+
];
|
|
1758
|
+
const sender = fromMatch ? (parseAddrs(fromMatch[1])[0] || account.email) : account.email;
|
|
1759
|
+
if (recipients.length === 0)
|
|
1760
|
+
throw new Error("No recipients");
|
|
1761
|
+
const rawToSend = raw.replace(/^Bcc:.*\r?\n/mi, "");
|
|
1762
|
+
this.saveSendingCopy(accountId, rawToSend, "sent");
|
|
1763
|
+
await transport.sendMail({ envelope: { from: sender, to: recipients }, raw: rawToSend });
|
|
1764
|
+
console.log(` [smtp] ${accountId}: sent to ${recipients.join(", ")}`);
|
|
1765
|
+
}
|
|
1700
1766
|
/** Process Outbox — send pending messages with flag-based interlock */
|
|
1701
1767
|
async processOutbox(accountId) {
|
|
1702
1768
|
const outboxFolder = this.findFolder(accountId, "outbox");
|
|
@@ -1705,7 +1771,7 @@ export class ImapManager extends EventEmitter {
|
|
|
1705
1771
|
// Skip if this account's sync is failing — don't pile up connections
|
|
1706
1772
|
if (this.connectionBackoff.has(accountId) && Date.now() < (this.connectionBackoff.get(accountId) || 0))
|
|
1707
1773
|
return;
|
|
1708
|
-
// Gmail
|
|
1774
|
+
// Gmail: skip IMAP outbox check — sending handled by processLocalQueue which sends directly via SMTP
|
|
1709
1775
|
if (this.isGmailAccount(accountId))
|
|
1710
1776
|
return;
|
|
1711
1777
|
const settings = loadSettings();
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
From: Bob Frankston <Bob.Frankston@Gmail.com>
|
|
2
|
+
To: David P. Reed <dpreed@deepplum.com>
|
|
3
|
+
Cc: Brian DeLacey <bdelacey@gmail.com>
|
|
4
|
+
Bcc: bob@bob.ma
|
|
5
|
+
Subject: Re: Converting POPFile from perl to Python
|
|
6
|
+
Date: Wed, 08 Apr 2026 01:02:13 GMT
|
|
7
|
+
Message-ID: <1775610133517.ug0o0gii4wm@Gmail.com>
|
|
8
|
+
MIME-Version: 1.0
|
|
9
|
+
Content-Type: text/html; charset=UTF-8
|
|
10
|
+
Content-Transfer-Encoding: base64
|
|
11
|
+
|
|
12
|
+
PHA+SSdtIHJlc3BvbmRpbmcgdXNpbmcgbXkgZW1haWwgcHJvZ3JhbS48L3A+PHA+PGJyPjwvcD48
|
|
13
|
+
cD5UaGVyZSBpcyBzbyBtdWNoIHRvIGNhdGNoIHVwIHdpdGggLS0gdGhlIGdvb2QgYW5kIHRoZSBi
|
|
14
|
+
YWQuIE9uZSBvYnNlcnZhdGlvbiBpcyB0aGF0IGl0IGxlYXJuZWQgdG8gbXVjaCB0byBwcm9ncmFt
|
|
15
|
+
IGxpa2UgYSBodW1hbiBhbmQgbm90IGVub3VnaC4gT25lIGh1bW9yb3VzIHRoaW5nIGlzIHdoZW4g
|
|
16
|
+
aXQgZXN0aW1hdGVzIGEgdGFzayB3aWxsIHRha2UgYSB3ZWVrIGJ1dCBpdCBkb2VzIGl0IGluIGZp
|
|
17
|
+
dmUgbWludXRlcyBiZWNhdXNlIGl0IGlzIHVzaW5nIHN0YW5kYXJkIG1lYXN1cmVzLiBJdCdzIGRl
|
|
18
|
+
c2lnbiBzZW5zZSBpcyBmdXJzaGl0IGJ1dCBpZiB5b3Ugd29yayB3aXRoIGl0IGFuZCBnZXQgcGFz
|
|
19
|
+
dCB0aGUgSmVuZ2EgbW9kZSB5b3UgY2FuIGdldCBsb3RzIGRvbmUuIEZvciBleGFtcGxlLCBpbWFw
|
|
20
|
+
IGRvZXNuJ3Qgd29yayB3ZWxsIHdpdGggZ21haWwgYnV0IGl0IHNhaWQgdGhhdCBhZGRpbmcgZ21h
|
|
21
|
+
aWwgd2FzIGEgbWFqb3IgcHJvamVjdCBidXQgSSBzYWlkIGRvIGl0IGFueXdheSBhbmQgZml2ZSBt
|
|
22
|
+
aW51dGVzIGxhdGVyIGl0IHdhcyBkb25lLiBucG0gaW5zdGFsbCAtZyBAYm9iZnJhbmtzdG9uL21h
|
|
23
|
+
aWx4IGlzbid0IHF1aXQgcmVhZHkgZm9yIHByaW1lIHRpbWUuPC9wPjxwPjxicj48L3A+PHA+U3Rp
|
|
24
|
+
bGwgYSBodWdlIHRvZG8gbGlzdCAoaW4gdGhlIG1haWx4IGRpciBpZiB5b3UncmUgY3VyaW91cy4g
|
|
25
|
+
SXQgd2lsbCB3YW50IHRvIHN0YXJ0IHdpdGggYSBnbWFpbCBhZGRyZXNzIGFuZCBjcmVhdGVzIGEg
|
|
26
|
+
bWFpbHggZGlyZWN0b3J5IG9uIGdkcml2ZS4gQSBnYWluLCBzdGlsbCBsb3RzIG9mIGlzc3VlcyB3
|
|
27
|
+
aGljaCB5b3UgY2FuIHJlcG9ydCB0byBtZSBidXQgaXQgaXMgY29taW5nIHRvZ2V0aGVyLjwvcD48
|
|
28
|
+
cD48YnI+PC9wPjxwPk9uIDQvNy8yMDI2LCA1OjA0OjU3IFBNLCBEYXZpZCBQLiBSZWVkICZsdDtk
|
|
29
|
+
cHJlZWRAZGVlcHBsdW0uY29tJmd0OyB3cm90ZTo8L3A+PGRpdiBjbGFzcz0icWwtY29kZS1ibG9j
|
|
30
|
+
ay1jb250YWluZXIiIHNwZWxsY2hlY2s9ImZhbHNlIj48ZGl2IGNsYXNzPSJxbC1jb2RlLWJsb2Nr
|
|
31
|
+
IiBkYXRhLWxhbmd1YWdlPSJwbGFpbiI+Rmlyc3Qgb3ZlcmFsbCB0ZXN0IGJlZ2FuIGxvb3Bpbmcg
|
|
32
|
+
Zm9yZXZlci4gSSBzdG9wcGVkIGl0LCBhbmQgaW5xdWlyZWQgd2hhdCB0aGUgaGVsbCwgYW5kPC9k
|
|
33
|
+
aXY+PGRpdiBjbGFzcz0icWwtY29kZS1ibG9jayIgZGF0YS1sYW5ndWFnZT0icGxhaW4iPmhlcmUg
|
|
34
|
+
aXMgd2hhdCBpdCBzYXlzLiBHYWFoLiBUaGlzIGlzIG5vdCBleHBsYWluaW5nIG11Y2gsIGFuZCBp
|
|
35
|
+
dCB3cm90ZSB0aGlzIGNvZGUuLi48L2Rpdj48ZGl2IGNsYXNzPSJxbC1jb2RlLWJsb2NrIiBkYXRh
|
|
36
|
+
LWxhbmd1YWdlPSJwbGFpbiI+PGJyPjwvZGl2PjxkaXYgY2xhc3M9InFsLWNvZGUtYmxvY2siIGRh
|
|
37
|
+
dGEtbGFuZ3VhZ2U9InBsYWluIj4tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLTwvZGl2Pjxk
|
|
38
|
+
aXYgY2xhc3M9InFsLWNvZGUtYmxvY2siIGRhdGEtbGFuZ3VhZ2U9InBsYWluIj5UaGUgdGVzdCBo
|
|
39
|
+
YW5ncyBiZWNhdXNlICNoYW5kbGVDbGllbnQgaW4gUE9QM1Byb3h5IHRocm93cyBiZWZvcmUgc2Vu
|
|
40
|
+
ZGluZyB0aGUgYmFubmVyLiBUaGUgY3VscHJpdCBpcyB0aGlzIGRlYWQgY29kZTo8L2Rpdj48ZGl2
|
|
41
|
+
IGNsYXNzPSJxbC1jb2RlLWJsb2NrIiBkYXRhLWxhbmd1YWdlPSJwbGFpbiI+PGJyPjwvZGl2Pjxk
|
|
42
|
+
aXYgY2xhc3M9InFsLWNvZGUtYmxvY2siIGRhdGEtbGFuZ3VhZ2U9InBsYWluIj4gIGNvbnN0IGNs
|
|
43
|
+
aWVudFdyaXRlciA9IG5ldyBXcml0YWJsZVN0cmVhbURlZmF1bHRXcml0ZXIoPC9kaXY+PGRpdiBj
|
|
44
|
+
bGFzcz0icWwtY29kZS1ibG9jayIgZGF0YS1sYW5ndWFnZT0icGxhaW4iPiAgICBjbGllbnQud3Jp
|
|
45
|
+
dGFibGUuZ2V0V3JpdGVyKCkucmVsZWFzZUxvY2soKSBhcyB1bmtub3duIGFzIFdyaXRhYmxlU3Ry
|
|
46
|
+
ZWFtPC9kaXY+PGRpdiBjbGFzcz0icWwtY29kZS1ibG9jayIgZGF0YS1sYW5ndWFnZT0icGxhaW4i
|
|
47
|
+
PiAgKTs8L2Rpdj48ZGl2IGNsYXNzPSJxbC1jb2RlLWJsb2NrIiBkYXRhLWxhbmd1YWdlPSJwbGFp
|
|
48
|
+
biI+PGJyPjwvZGl2PjxkaXYgY2xhc3M9InFsLWNvZGUtYmxvY2siIGRhdGEtbGFuZ3VhZ2U9InBs
|
|
49
|
+
YWluIj4gIHJlbGVhc2VMb2NrKCkgcmV0dXJucyB2b2lkLiBQYXNzaW5nIHZvaWQgdG8gV3JpdGFi
|
|
50
|
+
bGVTdHJlYW1EZWZhdWx0V3JpdGVyIHRocm93cyBhIFR5cGVFcnJvciwgd2hpY2ggaXMgc3dhbGxv
|
|
51
|
+
d2VkIGJ5IHRoZTwvZGl2PjxkaXYgY2xhc3M9InFsLWNvZGUtYmxvY2siIGRhdGEtbGFuZ3VhZ2U9
|
|
52
|
+
InBsYWluIj4gIGFjY2VwdCBsb29wJ3MgLmNhdGNoIOKAlCBzbyB0aGUgY2xpZW50IG5ldmVyIHJl
|
|
53
|
+
Y2VpdmVzIHRoZSBiYW5uZXIgYW5kIHdhaXRzIGZvcmV2ZXIuIGNsaWVudFdyaXRlciBpcyBhbHNv
|
|
54
|
+
IG5ldmVyIHVzZWQuPC9kaXY+PGRpdiBjbGFzcz0icWwtY29kZS1ibG9jayIgZGF0YS1sYW5ndWFn
|
|
55
|
+
ZT0icGxhaW4iPiAgRml4OjwvZGl2PjxkaXYgY2xhc3M9InFsLWNvZGUtYmxvY2siIGRhdGEtbGFu
|
|
56
|
+
Z3VhZ2U9InBsYWluIj48YnI+PC9kaXY+PGRpdiBjbGFzcz0icWwtY29kZS1ibG9jayIgZGF0YS1s
|
|
57
|
+
YW5ndWFnZT0icGxhaW4iPjxicj48L2Rpdj48ZGl2IGNsYXNzPSJxbC1jb2RlLWJsb2NrIiBkYXRh
|
|
58
|
+
LWxhbmd1YWdlPSJwbGFpbiI+PGJyPjwvZGl2PjwvZGl2Pg==
|