tutanota/buildSrc/updateLibs.js
sug f11e59672e
improve inbox rule handling and run spam prediction after inbox rules
Instead of applying inbox rules based on the unread mail state in the
inbox folder, we introduce the new ProcessingState enum on
the mail type. If a mail has been processed by the leader client, which
is checking for matching inbox rules, the ProcessingState is
updated. If there is a matching rule the flag is updated through the
MoveMailService, if there is no matching rule, the flag is updated
using the ClientClassifierResultService. Both requests are
throttled / debounced. After processing inbox rules, spam prediction
is conducted for mails that have not yet been moved by an inbox rule.
The ProcessingState for not matching ham mails is also updated using
the ClientClassifierResultService.

This new inbox rule handing solves the following two problems:
 - when clicking on a notification it could still happen,
   that sometimes the inbox rules where not applied
 - when the inbox folder had a lot of unread mails, the loading time did
   massively increase, since inbox rules were re-applied on every load

Co-authored-by: amm <amm@tutao.de>
Co-authored-by: Nick <nif@tutao.de>
Co-authored-by: das <das@tutao.de>
Co-authored-by: abp <abp@tutao.de>
Co-authored-by: jhm <17314077+jomapp@users.noreply.github.com>
Co-authored-by: map <mpfau@users.noreply.github.com>
Co-authored-by: Kinan <104761667+kibibytium@users.noreply.github.com>
2025-10-22 09:40:45 +02:00

240 lines
7.8 KiB
JavaScript

/**
* Copies all currently used libraries from node_modules into libs.
*
* We do this to be able to audit changes in the libraries and not rely on npm for checksums.
*/
import fs from "fs-extra"
import path, { dirname } from "node:path"
import { fileURLToPath } from "node:url"
import { rollup } from "rollup"
import { nodeResolve } from "@rollup/plugin-node-resolve"
import commonjs from "@rollup/plugin-commonjs"
import child_process from "node:child_process"
import { promisify } from "node:util"
import alias from "@rollup/plugin-alias"
const __dirname = dirname(fileURLToPath(import.meta.url))
async function stripCommentsFromTensorflow() {
let str = fs.readFileSync("libs/tensorflow.js").toString()
str = str.replace(/\/\*[\s\S]*?\*\/|(?<=[^:])\/\/.*|^\/\/.*/g, "")
fs.writeFileSync("libs/tensorflow-stripped.js", str)
}
export async function updateLibs() {
await copyToLibs(clientDependencies)
await stripCommentsFromTensorflow()
}
/**
* Should correspond to {@link import("./RollupConfig").dependencyMap}
*
* @typedef {"rollupWeb" | "rollupTF" | "rollupDesktop" | "copy"} BundlingStrategy
* @typedef {{src: string, target: string, bundling: BundlingStrategy, banner?: string, patch?: string}} DependencyDescription
* @type Array<DependencyDescription>
*
*/
const clientDependencies = [
// mithril is patched manually to remove some unused parts
// "../node_modules/mithril/mithril.js",
{ src: "../node_modules/mithril/stream/stream.js", target: "stream.js", bundling: "copy" },
// squire is patched manually to fix issues
// "../node_modules/squire-rte/dist/squire-raw.mjs",
{ src: "../node_modules/dompurify/dist/purify.es.mjs", target: "purify.js", bundling: "copy" },
{ src: "../node_modules/linkifyjs/dist/linkify.mjs", target: "linkify.js", bundling: "copy" },
{ src: "../node_modules/linkify-html/dist/linkify-html.mjs", target: "linkify-html.js", bundling: "copy" },
{ src: "../node_modules/luxon/build/es6/luxon.js", target: "luxon.js", bundling: "copy" },
{ src: "../node_modules/jsqr/dist/jsQR.js", target: "jsQR.js", bundling: "copy" },
{ src: "../node_modules/jszip/dist/jszip.js", target: "jszip.js", bundling: "rollupWeb" },
{ src: "../node_modules/cborg/cborg.js", target: "cborg.js", bundling: "rollupWeb" },
{ src: "../node_modules/qrcode-svg/lib/qrcode.js", target: "qrcode.js", bundling: "rollupWeb" },
{ src: "../node_modules/electron-updater/out/main.js", target: "electron-updater.mjs", bundling: "rollupDesktop" },
{ src: "../node_modules/@signalapp/sqlcipher/dist/index.mjs", target: "node-sqlcipher.mjs", bundling: "copy" },
{ src: "../node_modules/undici/index.js", target: "undici.mjs", bundling: "rollupDesktop" },
{ src: "../node_modules/@fingerprintjs/botd/dist/botd.esm.js", target: "botd.mjs", bundling: "rollupWeb", patch: "./libs/botd.patch" },
{ src: "../src/mail-app/workerUtils/spamClassification/tensorflow-custom.js", target: "tensorflow.js", bundling: "rollupTF" },
]
async function applyPatch() {
// rolldown gets confused when module.exports are used in an expression and wraps everything into a default export
// remove the problematic parts
console.log("applying a patch to undici")
const undiciPath = path.join(__dirname, "../node_modules/undici/index.js")
const contents = await fs.readFile(undiciPath, { encoding: "utf-8" })
const replaced = contents
.replace(
`const SqliteCacheStore = require('./lib/cache/sqlite-cache-store')
module.exports.cacheStores.SqliteCacheStore = SqliteCacheStore`,
"",
)
.replace(
`function install () {
globalThis.fetch = module.exports.fetch
globalThis.Headers = module.exports.Headers
globalThis.Response = module.exports.Response
globalThis.Request = module.exports.Request
globalThis.FormData = module.exports.FormData
globalThis.WebSocket = module.exports.WebSocket
globalThis.CloseEvent = module.exports.CloseEvent
globalThis.ErrorEvent = module.exports.ErrorEvent
globalThis.MessageEvent = module.exports.MessageEvent
globalThis.EventSource = module.exports.EventSource
}
module.exports.install = install`,
"",
)
await fs.writeFile(undiciPath, replaced, { encoding: "utf-8" })
}
/**
* applies a git patch file that was created as such:
* 1. get the unpatched version of whatever library you want to add / change
* 2. make a commit with the changes that you want to make
* 3. format the patch by running:
* git format-patch -k --stdout HEAD~1..HEAD > ./libs/changes.patch
* 4. revert the commit by running:
* git reset --hard HEAD~1
* 5. commit the generated ./libs.changes file
*/
async function applyGitPatch(patchFile) {
if (process.platform === "win32") return
const exec = promisify(child_process.exec)
console.log(`applying a patch to ${patchFile}`)
await exec(`git apply ${patchFile}`)
}
/**
* @param dependencies {Array<DependencyDescription>}>}
* @return {Promise<void>}
*/
async function copyToLibs(dependencies) {
await applyPatch()
for (let { bundling, src, target, banner, patch } of dependencies) {
switch (bundling) {
case "copy":
await fs.copy(path.join(__dirname, src), path.join(__dirname, "../libs/", target))
break
case "rollupWeb":
await rollWebDep(src, target, banner)
break
case "rollupTF":
await rollupTensorFlow(src, target, banner)
break
case "rollupDesktop":
await rollDesktopDep(src, target, banner)
break
default:
throw new Error(`Unknown bundling strategy: ${bundling}`)
}
if (patch != null) {
await applyGitPatch(patch)
}
}
}
/**
* Will bundle web app dependencies starting at {@param src} into a single file at {@param target}.
* @type RollupFn
*/
async function rollWebDep(src, target, banner) {
const bundle = await rollup({ input: path.join(__dirname, src), plugins: [nodeResolve()] })
await bundle.write({ file: path.join(__dirname, "../libs", target), banner })
}
const logResolvePlugin = {
name: "log-resolve",
resolveId(source, importer) {
console.log(`Resolving: source='${source}', importer='${importer}'`)
return null
},
}
async function rollupTensorFlow(src, target, banner) {
const bundle = await rollup({
input: path.join(__dirname, src),
treeshake: {
moduleSideEffects: false,
preset: "smallest",
},
plugins: [
alias({
entries: [
{
find: /\.\/http/,
replacement: path.resolve(__dirname, "../libs/tensorflow-http-stub.js"),
},
{
find: /\.\/platforms\/.*/,
replacement: path.resolve(__dirname, "../libs/tensorflow-platform-stub.js"),
},
],
}),
// logResolvePlugin,
nodeResolve(),
commonjs(),
],
output: {
format: "esm",
},
})
await bundle.write({ file: path.join(__dirname, "../libs", target), banner })
}
/**
* @typedef {(src: string, target: string, banner: string | undefined) => Promise<void>} RollupFn
* rollup desktop dependencies with their dependencies into a single esm file
*
* specifically, electron-updater is importing some electron internals directly, so we made a comprehensive list of
* exclusions to not roll up.
*
* @type RollupFn
*/
async function rollDesktopDep(src, target, banner) {
const bundle = await rollup({
input: path.join(__dirname, src),
makeAbsoluteExternalsRelative: true,
external: [
// we handle .node imports ourselves
/\.node$/,
"assert",
"child_process",
"constants",
"crypto",
"electron",
"events",
"fs",
"http",
"https",
"os",
"path",
"stream",
"string_decoder",
"tty",
"url",
"util",
"zlib",
/.*sqlite-cache-store$/,
],
plugins: [
nodeResolve({ preferBuiltins: true }),
commonjs({
ignore: ["node:sqlite"],
}),
],
onwarn: (warning, defaultHandler) => {
if (warning.code === "CIRCULAR_DEPENDENCY") {
return // Ignore circular dependency warnings
}
defaultHandler(warning)
},
})
await bundle.write({
file: path.join(__dirname, "../libs", target),
format: "es",
// another ugly hack for better-sqlite
banner,
})
}