Add additional jmap API functionality.

This commit is contained in:
Andrew Pietila 2025-01-20 00:02:31 -06:00
parent 9d4d264a09
commit a2740f6359
8 changed files with 177 additions and 16 deletions

1
.gitignore vendored
View file

@ -1,3 +1,4 @@
sendgrid.env
messages/
node_modules/
state.integer

3
.gitmodules vendored Normal file
View file

@ -0,0 +1,3 @@
[submodule "static/jmapjs"]
path = static/jmapjs
url = https://github.com/jmapio/jmap-demo-webmail

View file

@ -1,4 +1,4 @@
import MimeHeader from "./MimeHeader";
import MimeHeader from "./MimeHeader.js";
class MimeMessage {
constructor(message) {

23
package-lock.json generated
View file

@ -8,6 +8,7 @@
"@sendgrid/mail": "^8.1.4",
"body-parser": "^1.20.3",
"cookie-parser": "^1.4.7",
"cors": "^2.8.5",
"express": "^4.21.2",
"formidable": "^3.5.2"
}
@ -224,6 +225,19 @@
"integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==",
"license": "MIT"
},
"node_modules/cors": {
"version": "2.8.5",
"resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz",
"integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==",
"license": "MIT",
"dependencies": {
"object-assign": "^4",
"vary": "^1"
},
"engines": {
"node": ">= 0.10"
}
},
"node_modules/debug": {
"version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
@ -702,6 +716,15 @@
"node": ">= 0.6"
}
},
"node_modules/object-assign": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
"integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/object-inspect": {
"version": "1.13.3",
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.3.tgz",

View file

@ -3,6 +3,7 @@
"@sendgrid/mail": "^8.1.4",
"body-parser": "^1.20.3",
"cookie-parser": "^1.4.7",
"cors": "^2.8.5",
"express": "^4.21.2",
"formidable": "^3.5.2"
}

151
server.js
View file

@ -6,25 +6,54 @@ import fs from "node:fs";
import cookieParser from 'cookie-parser';
import path from 'node:path';
import { fileURLToPath } from "node:url";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
import cors from 'cors';
import bodyParser from "body-parser";
import MimeMessage from "./lib/MimeMessage.js";
import MimeHeader from "./lib/MimeHeader.js";
app.use(cors());
app.use(cookieParser());
let state = +fs.readFileSync("state.integer", "utf-8");
console.log(`State: ${state.toString()}`);
// app.use((req, res, next) => {
// console.log({url: req.url, headers: req.headers});
// req.on("data", (data) => {
// console.log(data.toString("utf8"));
// })
// next();
// })
app.post('/sendgrid/ingress', (req, res) => {
const form = formidable({});
form.parse(req, (err, fields, files) => {
var message = "";
if ( fields.email ) {
if ( typeof fields.email === "string") {
message = fields.email;
} else {
message = fields.email.join("");
}
}
var message = fields.email?.[0];
fs.writeFileSync(`messages/${new Date().toISOString()}.eml`, message);
fs.writeFileSync(`messages/${new Date().toISOString()}.json`, JSON.stringify(fields));
var messageObj = new MimeMessage(message);
var envelope = JSON.parse(fields.envelope?.[0]??"{}");
messageObj.prependHeader(new MimeHeader(`Received: from ${req.headers["x-forwarded-for"]} by andrewpietila.com with HTTPS-API id ${state} for ${envelope.to??"unknown"}; ${new Date().toISOString()}`));
messageObj.prependHeader(new MimeHeader(`X-Auth-Check: SPF ${fields.SPF?.[0]??"missing"}`));
messageObj.prependHeader(new MimeHeader(`X-Auth-Check: DKIM ${fields.dkim?.[0]??"missing"}`));
messageObj.prependHeader(new MimeHeader(`Return-Path: <${envelope.from??"unknown"}>`));
fs.writeFileSync(`messages/${new Date().toISOString()}.eml`, messageObj.toString());
fs.writeFileSync(`messages/${new Date().toISOString()}-headers.json`, JSON.stringify(req.headers, null, 4));
fs.writeFileSync(`messages/${new Date().toISOString()}.json`, JSON.stringify(fields, null, 4));
res.status(204);
res.end();
state++;
fs.writeFileSync("state.integer", state.toString(), {encoding: "utf-8"});
})
});
@ -43,7 +72,8 @@ app.all("/.well-known/jmap", (req, res) => {
// https://jmap.io/spec-core.html#the-jmap-session-resource
// TODO: Authenticated
app.get("/jmap/session", (req, res) => {
if ( req.cookies.authenticated == "true" )
// TODO: HTTP Basic auth.
// if ( req.cookies.authenticated == "true" )
res.json({
"capabilities": {
"urn:ietf:params:jmap:core": {
@ -60,6 +90,20 @@ app.get("/jmap/session", (req, res) => {
"i;unicode-casemap"
]
},
"urn:ietf:params:jmap:mail": {
maxMailboxesPerEmail: null,
maxMailboxDepth: null,
maxSizeMailboxName: 100,
maxSizeAttachmentsPerEmail: 50_000_000,
emailQuerySortOptions: [
"receivedAt"
],
mayCreateTopLevelMailbox: false
},
"urn:ietf:params:jmap:submission": {
maxDelayedSend: 0,
submissionExtensions: []
}
},
"accounts": {
"andrew@andrewpietila.com": {
@ -78,10 +122,89 @@ app.get("/jmap/session", (req, res) => {
"eventSourceUrl": "https://andrewpietila.com/api/jmap/eventsource/?types={types}&closeafter={closeafter}&ping={ping}",
"state": "75128aab4b1b"
});
else {
res.status(404);
res.end();
// else {
// res.status(404);
// res.end();
// }
});
function isEmpty(obj) {
for (var prop in obj) {
if ( Object.prototype.hasOwnProperty.call(obj, prop) ) {
return false;
}
}
return true;
}
app.post("/api/jmap/api/", bodyParser.json(), (req, res) => {
const responses = req.body.methodCalls.map(element => {
if (element[0] === "Identity/get" && isEmpty(element[1]) ) {
return [ "Identity/get",
{
accountId: "andrew@andrewpietila.com",
state: state.toString(),
"list": [
{
"id": "andrew@andrewpietila.com",
"name": "Andrew Pietila",
"email": "andrew@andrewpietla.com",
"replyTo": null,
"bcc": null,
textSignature: "",
htmlSignature: "",
mayDelete: true
}
]
},
element[2]
]
} else if ( element[0] === "Mailbox/get" && isEmpty(element[1]) ) {
return ["Mailbox/get",
{
accountId: "andrew@andrewpietila.com",
state: state.toString(),
list: [{
id: "Inbox",
name: "Inbox",
parentId: null,
role: "inbox",
sortOrder: 0,
totalEmails: 0, // TODO: Index messages.
unreadEmails: 0, // TODO: Index messages.
totalThreads: 0, // TODO: Index messages.
unreadThreads: 0, // TODO: Index messages.
myRights: {
mayReadItems: true,
mayAddItems: true,
mayRemoveItems: true,
maySetSeen: true,
maySetKeyword: false,
mayCreateChild: false, // TODO: Implement child folders.
mayRename: false, // TODO: Implement renaming folders.
mayDelete: true,
maySubmit: true
},
isSubscribed: true // TODO: Handle subscriptions.
}],
},
element[2]
]
}
return [
"error",
{
type: "unknownMethod"
},
element[2]
]
});
res.json({"methodResponses": responses, sessionState: "1"});
})
app.use("/static", express.static(__dirname + "/static"));
app.listen(8080);

1
static/jmapjs Submodule

@ -0,0 +1 @@
Subproject commit 2d59257506c634af1c2bf2a6325cc2aac59831b6

9
static/login.html Normal file
View file

@ -0,0 +1,9 @@
<!DOCTYPE html>
<html lang="en_US">
<head>
<title>Login</title>
</head>
<form action="/mail/login" enctype="multipart/form-data" method="post" >
<input type="submit" value="Upload" />
</form>
</html>