Compare commits
4 Commits
d8ee3d5f0f
...
1e9dae15f1
Author | SHA1 | Date |
---|---|---|
|
1e9dae15f1 | |
|
dfca32eb36 | |
|
5a36e46da5 | |
|
a412b6a0a4 |
|
@ -21,6 +21,7 @@
|
||||||
"bootstrap": "^5.3.3",
|
"bootstrap": "^5.3.3",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-bootstrap": "^2.10.4",
|
"react-bootstrap": "^2.10.4",
|
||||||
|
"react-bootstrap-icons": "^1.11.4",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
"react-scripts": "5.0.1",
|
"react-scripts": "5.0.1",
|
||||||
"sass": "^1.77.8",
|
"sass": "^1.77.8",
|
||||||
|
@ -14746,6 +14747,17 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/react-bootstrap-icons": {
|
||||||
|
"version": "1.11.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-bootstrap-icons/-/react-bootstrap-icons-1.11.4.tgz",
|
||||||
|
"integrity": "sha512-lnkOpNEZ/Zr7mNxvjA9efuarCPSgtOuGA55XiRj7ASJnBjb1wEAdtJOd2Aiv9t07r7FLI1IgyZPg9P6jqWD/IA==",
|
||||||
|
"dependencies": {
|
||||||
|
"prop-types": "^15.7.2"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">=16.8.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/react-dev-utils": {
|
"node_modules/react-dev-utils": {
|
||||||
"version": "12.0.1",
|
"version": "12.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/react-dev-utils/-/react-dev-utils-12.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/react-dev-utils/-/react-dev-utils-12.0.1.tgz",
|
||||||
|
|
|
@ -16,6 +16,7 @@
|
||||||
"bootstrap": "^5.3.3",
|
"bootstrap": "^5.3.3",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-bootstrap": "^2.10.4",
|
"react-bootstrap": "^2.10.4",
|
||||||
|
"react-bootstrap-icons": "^1.11.4",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
"react-scripts": "5.0.1",
|
"react-scripts": "5.0.1",
|
||||||
"sass": "^1.77.8",
|
"sass": "^1.77.8",
|
||||||
|
|
|
@ -88,6 +88,10 @@ class EmailContent extends React.Component<
|
||||||
Received
|
Received
|
||||||
</Form.Label>
|
</Form.Label>
|
||||||
<Col sm={10}>{email.receivedAt}</Col>
|
<Col sm={10}>{email.receivedAt}</Col>
|
||||||
|
<Form.Label column sm={2}>
|
||||||
|
Sent
|
||||||
|
</Form.Label>
|
||||||
|
<Col sm={10}>{email.sentAt}</Col>
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
</Form>
|
</Form>
|
||||||
</Row>
|
</Row>
|
||||||
|
|
|
@ -1,3 +1,7 @@
|
||||||
|
import { Trash } from "react-bootstrap-icons";
|
||||||
|
import Button from "react-bootstrap/Button";
|
||||||
|
import ButtonGroup from "react-bootstrap/ButtonGroup";
|
||||||
|
import ButtonToolbar from "react-bootstrap/ButtonToolbar";
|
||||||
import Placeholder from "react-bootstrap/Placeholder";
|
import Placeholder from "react-bootstrap/Placeholder";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
|
@ -55,6 +59,21 @@ class EmailSummary extends React.Component<
|
||||||
stub.subject}
|
stub.subject}
|
||||||
</span>
|
</span>
|
||||||
</a>
|
</a>
|
||||||
|
<ButtonToolbar>
|
||||||
|
<ButtonGroup className="me-2">
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
this.props.client.emailMoveTrash(
|
||||||
|
this.props.account,
|
||||||
|
this.props.emailId,
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Trash />
|
||||||
|
</Button>
|
||||||
|
<Button>2</Button>
|
||||||
|
</ButtonGroup>
|
||||||
|
</ButtonToolbar>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,10 +3,19 @@
|
||||||
* None of the dependencies should leak, in types or otherwise
|
* None of the dependencies should leak, in types or otherwise
|
||||||
*/
|
*/
|
||||||
import * as base64 from "base-64";
|
import * as base64 from "base-64";
|
||||||
|
import * as jmaptypes from "./jmap-client-ts/src/types";
|
||||||
import * as jmapclient from "./jmap-client-ts/src";
|
import * as jmapclient from "./jmap-client-ts/src";
|
||||||
import { FetchTransport } from "./jmap-client-ts/src/utils/fetch-transport";
|
import { FetchTransport } from "./jmap-client-ts/src/utils/fetch-transport";
|
||||||
|
|
||||||
import { IAccount, IEmail, IEmailStub, IMailbox, ISession } from "./types";
|
import {
|
||||||
|
IAccount,
|
||||||
|
IEmail,
|
||||||
|
IEmailStub,
|
||||||
|
IMailbox,
|
||||||
|
ISession,
|
||||||
|
MailboxRole,
|
||||||
|
PushMessage,
|
||||||
|
} from "./types";
|
||||||
|
|
||||||
type Callback = () => void;
|
type Callback = () => void;
|
||||||
|
|
||||||
|
@ -21,9 +30,40 @@ export interface ClientState {
|
||||||
emailStubs: Set<string>;
|
emailStubs: Set<string>;
|
||||||
mailboxes: Set<string>;
|
mailboxes: Set<string>;
|
||||||
};
|
};
|
||||||
|
mailboxes: {
|
||||||
|
trash: IMailbox;
|
||||||
|
} | null;
|
||||||
session: ISession | null;
|
session: ISession | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class Account implements IAccount {
|
||||||
|
id: string;
|
||||||
|
|
||||||
|
accountCapabilities: { [key: string]: jmaptypes.IMailCapabilities };
|
||||||
|
isPersonal: boolean;
|
||||||
|
isReadOnly: boolean;
|
||||||
|
mailboxes: Array<IMailbox> | null;
|
||||||
|
name: string;
|
||||||
|
|
||||||
|
constructor(id: string, props: jmaptypes.IAccount) {
|
||||||
|
this.id = id;
|
||||||
|
this.accountCapabilities = props.accountCapabilities;
|
||||||
|
this.isPersonal = props.isPersonal;
|
||||||
|
this.isReadOnly = props.isReadOnly;
|
||||||
|
this.mailboxes = null;
|
||||||
|
this.name = props.name;
|
||||||
|
}
|
||||||
|
mailboxByRole(role: MailboxRole): IMailbox | null {
|
||||||
|
if (this.mailboxes === null) return null;
|
||||||
|
for (let i = 0; i < this.mailboxes.length; i++) {
|
||||||
|
const m = this.mailboxes[i];
|
||||||
|
if (m.role === role) {
|
||||||
|
return m;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
export default class Client {
|
export default class Client {
|
||||||
// All objects which currently are listening for changes
|
// All objects which currently are listening for changes
|
||||||
callbacks: Array<Callback> = [];
|
callbacks: Array<Callback> = [];
|
||||||
|
@ -34,6 +74,7 @@ export default class Client {
|
||||||
emailStubs: new Set(),
|
emailStubs: new Set(),
|
||||||
mailboxes: new Set(),
|
mailboxes: new Set(),
|
||||||
},
|
},
|
||||||
|
mailboxes: null,
|
||||||
session: null,
|
session: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -67,6 +108,43 @@ export default class Client {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
emailMoveTrash(account: IAccount, emailId: string) {
|
||||||
|
if (this.jclient === null) return;
|
||||||
|
console.log("Trashing", emailId);
|
||||||
|
const email = this.emailStub(emailId);
|
||||||
|
if (email === null) return;
|
||||||
|
const trashMailbox = account.mailboxByRole("trash");
|
||||||
|
if (trashMailbox === null) {
|
||||||
|
console.error(
|
||||||
|
"Cannot trash ",
|
||||||
|
emailId,
|
||||||
|
" because ",
|
||||||
|
account.id,
|
||||||
|
" does not have a 'trash' mailbox",
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let mailboxIds = Object.keys(email.mailboxIds).reduce(
|
||||||
|
(acc, key) => {
|
||||||
|
acc[key as keyof typeof email.mailboxIds] = false;
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
{} as Record<keyof typeof email.mailboxIds, boolean>,
|
||||||
|
);
|
||||||
|
mailboxIds[trashMailbox.id] = true;
|
||||||
|
const props = {
|
||||||
|
accountId: account.id,
|
||||||
|
update: {
|
||||||
|
[email.id]: {
|
||||||
|
mailboxIds: mailboxIds,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
this.jclient.email_set(props).then((response) => {
|
||||||
|
console.log("Trashed", emailId);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Ensure we have the full email content
|
// Ensure we have the full email content
|
||||||
ensureEmailContent(accountId: string, emailId: string) {
|
ensureEmailContent(accountId: string, emailId: string) {
|
||||||
if (this.state.session == null) return;
|
if (this.state.session == null) return;
|
||||||
|
@ -98,7 +176,9 @@ export default class Client {
|
||||||
|
|
||||||
email(emailId: string): IEmail | null {
|
email(emailId: string): IEmail | null {
|
||||||
if (this.state.session == null) return null;
|
if (this.state.session == null) return null;
|
||||||
return this.state.session.emails[emailId];
|
const result = this.state.session.emails[emailId];
|
||||||
|
if (result === undefined) return null;
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
emailGetContent(accountId: string, emailId: string) {
|
emailGetContent(accountId: string, emailId: string) {
|
||||||
|
@ -148,7 +228,7 @@ export default class Client {
|
||||||
.email_get({
|
.email_get({
|
||||||
accountId: accountId,
|
accountId: accountId,
|
||||||
ids: [emailId],
|
ids: [emailId],
|
||||||
properties: ["from", "receivedAt", "subject"],
|
properties: ["from", "mailboxIds", "receivedAt", "subject"],
|
||||||
})
|
})
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
console.log(msg, "response", response);
|
console.log(msg, "response", response);
|
||||||
|
@ -163,6 +243,7 @@ export default class Client {
|
||||||
this.state.session.emailStubs[e.id] = {
|
this.state.session.emailStubs[e.id] = {
|
||||||
from: e.from,
|
from: e.from,
|
||||||
id: e.id,
|
id: e.id,
|
||||||
|
mailboxIds: e.mailboxIds,
|
||||||
receivedAt: e.receivedAt,
|
receivedAt: e.receivedAt,
|
||||||
subject: e.subject,
|
subject: e.subject,
|
||||||
};
|
};
|
||||||
|
@ -263,12 +344,16 @@ export default class Client {
|
||||||
if (!this.jclient) return;
|
if (!this.jclient) return;
|
||||||
|
|
||||||
const session = this.jclient.getSession();
|
const session = this.jclient.getSession();
|
||||||
|
// Subscribe to server-pushed events
|
||||||
|
if (session.eventSourceUrl) {
|
||||||
|
this._subscribeToEventSource(session.eventSourceUrl);
|
||||||
|
}
|
||||||
this.state.session = {
|
this.state.session = {
|
||||||
...session,
|
...session,
|
||||||
accounts: Object.fromEntries(
|
accounts: Object.fromEntries(
|
||||||
Object.entries(session.accounts).map(([key, account]) => [
|
Object.entries(session.accounts).map(([key, account]) => [
|
||||||
key,
|
key,
|
||||||
{ ...account, id: key.toString(), mailboxes: null },
|
new Account(key.toString(), account),
|
||||||
]),
|
]),
|
||||||
),
|
),
|
||||||
emails: {},
|
emails: {},
|
||||||
|
@ -277,4 +362,16 @@ export default class Client {
|
||||||
if (!this.state.session) return;
|
if (!this.state.session) return;
|
||||||
this._triggerChange("Session");
|
this._triggerChange("Session");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_subscribeToEventSource(url: string) {
|
||||||
|
// For typechecker
|
||||||
|
if (this.jclient === null) return;
|
||||||
|
const eventSourceUrl = url
|
||||||
|
.replace("{types}", "*")
|
||||||
|
.replace("{closeafter}", "no")
|
||||||
|
.replace("{ping}", "60");
|
||||||
|
this.jclient.subscribeToEvents(eventSourceUrl, (e) => {
|
||||||
|
console.log("Got an event!", e);
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
Subproject commit fdab37996a4709b13962b67d1aa49e85a8305755
|
Subproject commit 2ef5f5b7fa0a22a499bd32831ac24622f17e10e6
|
|
@ -1,8 +1,10 @@
|
||||||
import * as client from "./jmap-client-ts/src/types";
|
import * as client from "./jmap-client-ts/src/types";
|
||||||
|
|
||||||
|
export type MailboxIdMap = { [mailboxId: string]: boolean };
|
||||||
export interface IEmailStub {
|
export interface IEmailStub {
|
||||||
from: Array<client.IEmailAddress> | null;
|
from: Array<client.IEmailAddress> | null;
|
||||||
id: string;
|
id: string;
|
||||||
|
mailboxIds: MailboxIdMap;
|
||||||
receivedAt: string;
|
receivedAt: string;
|
||||||
subject: string;
|
subject: string;
|
||||||
}
|
}
|
||||||
|
@ -18,8 +20,20 @@ export interface IEmail extends client.IEmailProperties {
|
||||||
export interface IAccount extends client.IAccount {
|
export interface IAccount extends client.IAccount {
|
||||||
id: string;
|
id: string;
|
||||||
mailboxes: Array<IMailbox> | null;
|
mailboxes: Array<IMailbox> | null;
|
||||||
}
|
|
||||||
|
|
||||||
|
mailboxByRole(role: MailboxRole): IMailbox | null;
|
||||||
|
}
|
||||||
|
export type MailboxRole =
|
||||||
|
| "all"
|
||||||
|
| "archive"
|
||||||
|
| "drafts"
|
||||||
|
| "flagged"
|
||||||
|
| "important"
|
||||||
|
| "junk"
|
||||||
|
| "sent"
|
||||||
|
| "subscribed"
|
||||||
|
| "trash";
|
||||||
|
export type PushMessage = client.PushMessage;
|
||||||
export type AccountIdMap = { [accountId: string]: IAccount };
|
export type AccountIdMap = { [accountId: string]: IAccount };
|
||||||
export type EmailStubIdMap = { [emailId: string]: IEmailStub };
|
export type EmailStubIdMap = { [emailId: string]: IEmailStub };
|
||||||
export type EmailIdMap = { [emailId: string]: IEmail };
|
export type EmailIdMap = { [emailId: string]: IEmail };
|
||||||
|
|
Loading…
Reference in New Issue