Implement querying emails in mailbox.

Next step is auth implementation. Yay.
This commit is contained in:
Andrew Pietila 2025-01-21 22:14:02 -06:00
parent 87c785505d
commit dada829e6e
6 changed files with 1863 additions and 21 deletions

2
.gitignore vendored
View file

@ -2,3 +2,5 @@ sendgrid.env
messages/
node_modules/
state.integer
messageStore
dev.sqlite3

29
knexfile.js Normal file
View file

@ -0,0 +1,29 @@
// Update with your config settings.
/**
* @type { Object.<string, import("knex").Knex.Config> }
*/
module.exports = {
development: {
client: 'better-sqlite3',
connection: {
filename: './dev.sqlite3'
}
},
staging: {
client: 'better-sqlite3',
connection: {
filename: './staging.sqlite3'
}
},
production: {
client: 'better-sqlite3',
connection: {
filename: './production.sqlite3'
}
}
};

View file

@ -0,0 +1,20 @@
/**
* @param { import("knex").Knex } knex
* @returns { Promise<void> }
*/
exports.up = function(knex) {
return knex.schema.createTable("messages", (table) => {
table.increments("id", {primaryKey: true});
table.string("mailbox");
table.integer("state");
table.boolean("read");
})
};
/**
* @param { import("knex").Knex } knex
* @returns { Promise<void> }
*/
exports.down = function(knex) {
return knex.schema.dropTable("messages");
};

1738
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,10 +1,30 @@
{
"dependencies": {
"@sendgrid/mail": "^8.1.4",
"better-sqlite3": "^11.8.1",
"body-parser": "^1.20.3",
"cookie-parser": "^1.4.7",
"cors": "^2.8.5",
"express": "^4.21.2",
"formidable": "^3.5.2"
}
"formidable": "^3.5.2",
"knex": "^3.1.0",
"sqlite3": "^5.1.7"
},
"name": "andrewpietiladotcom",
"version": "0.0.1",
"description": "Website for andrewpietila.com",
"main": "server.js",
"directories": {
"lib": "lib"
},
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"start": "node server.js"
},
"repository": {
"type": "git",
"url": "ssh://git@forgejo.hax.social:2222/rallias/andrewpietiladotcom.git"
},
"author": "Andrew Pietila <andrew@andrewpietila.com>",
"license": "WTFPL"
}

View file

@ -18,6 +18,14 @@ import MimeMessage from "./lib/MimeMessage.js";
import MimeHeader from "./lib/MimeHeader.js";
import dns from 'node:dns';
import Knex from "knex";
const knex = Knex({
client: 'better-sqlite3',
connection: {
filename: './dev.sqlite3'
}
})
app.use(cors());
@ -26,18 +34,22 @@ 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});
app.use((req, res, next) => {
console.log({url: req.url, headers: req.headers});
// req.on("data", (data) => {
// console.log(data.toString("utf8"));
// })
// next();
// })
next();
})
app.post('/sendgrid/ingress', (req, res) => {
const form = formidable({});
form.parse(req, (err, fields, files) => {
var thisState = state;
state++;
fs.writeFileSync("state.integer", state.toString(), { encoding: "utf-8" });
var message = fields.email?.[0];
var messageObj = new MimeMessage(message);
@ -56,12 +68,11 @@ app.post('/sendgrid/ingress', (req, res) => {
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));
fs.writeFileSync(`messageStore/${thisState}.eml`, messageObj.toString());
await knex('messages').insert({mailbox: "INBOX", state: thisState, read: false});
res.status(204);
res.end();
state++;
fs.writeFileSync("state.integer", state.toString(), { encoding: "utf-8" });
})
})
});
@ -128,7 +139,7 @@ app.get("/jmap/session", (req, res) => {
"downloadUrl": "https://andrewpietila.com/api/jmap/download/{accountId}/{blobId}/{name}?accept={type}",
"uploadUrl": "https://andrewpietila.com/api/jmap/upload/{accountId}/",
"eventSourceUrl": "https://andrewpietila.com/api/jmap/eventsource/?types={types}&closeafter={closeafter}&ping={ping}",
"state": "75128aab4b1b"
"state": state.toString()
});
// else {
// res.status(404);
@ -146,10 +157,16 @@ function isEmpty(obj) {
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",
app.post("/api/jmap/api/", bodyParser.json(), async (req, res) => {
if ( req.header("authorization") !== "SUPER SECRET KEY" ) {
res.status(401);
res.end();
return;
}
console.log(JSON.stringify(req.body));
const responsesPromise = req.body.methodCalls.map(async element => {
if (element[0] === "Identity/get" && isEmpty(element[1])) {
return ["Identity/get",
{
accountId: "andrew@andrewpietila.com",
state: state.toString(),
@ -174,13 +191,13 @@ app.post("/api/jmap/api/", bodyParser.json(), (req, res) => {
accountId: "andrew@andrewpietila.com",
state: state.toString(),
list: [{
id: "Inbox",
id: "INBOX",
name: "Inbox",
parentId: null,
role: "inbox",
sortOrder: 0,
totalEmails: 0, // TODO: Index messages.
unreadEmails: 0, // TODO: Index messages.
totalEmails: +(await knex("messages").select().where("mailbox", "like", "INBOX").count('state')),
unreadEmails: +(await knex("messages").select().where("mailbox", "like", "INBOX").andWhere("read", "=", false).count('state')),
totalThreads: 0, // TODO: Index messages.
unreadThreads: 0, // TODO: Index messages.
myRights: {
@ -199,6 +216,21 @@ app.post("/api/jmap/api/", bodyParser.json(), (req, res) => {
},
element[2]
]
} else if ( element[0] === "Email/query" ) {
if ( element[1]["filter"] && element[1]["filter"]["inMailbox"] === "INBOX" ) {
// TODO: Implement proper sorting.
const messageIds = await knex("messages").select().where("mailbox", "like", "INBOX").orderBy("id", "asc");
return ["Email/query",
{
accountId: "andrew@andrewpietila.com",
queryState: state.toString(), // TODO: Maybe not a great idea.
canCalculateChanges: false,
position: 0,
ids: messageIds.map((messageId) => messageId.state.toString()),
},
element[2]
]
}
}
return [
@ -210,7 +242,10 @@ app.post("/api/jmap/api/", bodyParser.json(), (req, res) => {
]
});
res.json({"methodResponses": responses, sessionState: "1"});
const responses = await Promise.all(responsesPromise);
console.log(JSON.stringify({ "methodResponses": responses, sessionState: state.toString() }));
res.json({ "methodResponses": responses, sessionState: state.toString() });
})
app.use("/static", express.static(__dirname + "/static"));