diff --git a/.env-example b/.env-example index 87974a3..9336a85 100644 --- a/.env-example +++ b/.env-example @@ -4,4 +4,5 @@ IMAP_TLS=false IMAP_USERNAME=svc@domain.nl IMAP_PASSWORD=CHANGEME PLANKA_API_KEY=CHANGEME -PLANKA_URL=https://plankanban.github.io/planka/#/ \ No newline at end of file +PLANKA_URL=https://plankanban.github.io/planka/#/ +LOG_LEVEL=info \ No newline at end of file diff --git a/README.md b/README.md index d8204c1..ecbbca4 100644 --- a/README.md +++ b/README.md @@ -91,6 +91,7 @@ This application requires several environment variables to function correctly. B | `IMAP_PASSWORD` | The password for the IMAP account. | `undefined` | | `PLANKA_API_KEY` | The API key for authenticating with the Planka service. | `undefined` | | `PLANKA_URL` | The base URL of the Planka API. | `http://localhost:3000` | +| `LOG_LEVEL` | The log level for the application. | `info` | ### Note: Make sure to replace the default values with actual credentials before running the application. diff --git a/package.json b/package.json index 1d35828..4912845 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,8 @@ "dependencies": { "@gewis/planka-client": "github:GEWIS/planka-client#v1.1.0", "@hey-api/client-fetch": "^0.4.2", - "imapflow": "^1.0.169" + "imapflow": "^1.0.169", + "log4js": "^6.9.1" }, "devDependencies": { "@gewis/eslint-config": "https://github.com/GEWIS/eslint-config#v1.2.0", diff --git a/src/env.ts b/src/env.ts new file mode 100644 index 0000000..8902e23 --- /dev/null +++ b/src/env.ts @@ -0,0 +1,3 @@ +import { config } from 'dotenv'; + +config(); diff --git a/src/index.ts b/src/index.ts index c69a5ed..2f9100e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,21 +1,27 @@ -import { config } from 'dotenv'; -config(); - +import './env'; +import log4js from 'log4js'; import Mailer from './mailer'; import Planka from './planka'; +const logger = log4js.getLogger('Main'); +logger.level = process.env['LOG_LEVEL'] || 'info'; + // Main workflow async function main() { Planka.initialize(); const mailer: Mailer = new Mailer(); try { + logger.info('Starting process...'); const emails = await mailer.handleEmails(); const result = await Planka.processCards(emails); await mailer.handleResults(result); - console.log('Process completed successfully.'); + logger.info('Accepted', result.filter((r) => r.state === 'ACCEPTED').length, 'cards'); + logger.info('Rejected', result.filter((r) => r.state === 'REJECTED').length, 'cards'); } catch (error) { - console.error('An error occurred during the process:', error); + logger.error('An error occurred during the process:', error); + } finally { + logger.info('Process completed.'); } } diff --git a/src/mailer.ts b/src/mailer.ts index 78844dd..d0d0fdc 100644 --- a/src/mailer.ts +++ b/src/mailer.ts @@ -1,4 +1,5 @@ import { ImapFlow, Readable } from 'imapflow'; +import log4js from 'log4js'; export interface CardEmail { title: string; @@ -79,11 +80,15 @@ export default class Mailer { private readonly client: ImapFlow; private ROOT_PATH: string = process.env['IMAP_ROOT'] || 'API'; + private readonly logger = log4js.getLogger('Mailer'); + constructor() { + this.logger.level = process.env['LOG_LEVEL'] || 'warn'; this.client = new ImapFlow({ host: process.env['IMAP_HOST'] || 'localhost', port: Number(process.env['IMAP_PORT'] || '993'), secure: true, + logger: this.logger, auth: { user: process.env['IMAP_USERNAME'] || 'user', pass: process.env['IMAP_PASSWORD'], @@ -102,11 +107,14 @@ export default class Mailer { const emails: CardEmail[] = []; try { - for await (const message of this.client.fetch('1:*', { - envelope: true, - headers: true, - flags: true, - })) { + for await (const message of this.client.fetch( + {}, + { + envelope: true, + headers: true, + flags: true, + }, + )) { const headers = '' + message.headers; const plankaBoardId = extractPlankaBoardId(headers); const plankaListId = extractPlankaListId(headers); @@ -130,8 +138,9 @@ export default class Mailer { for (const email of emails) { email.body = await downloadEmailBody(this.client, email.uid); } + // eslint-disable-next-line @typescript-eslint/no-unused-vars } catch (e) { - console.error(e); + // noop } finally { lock.release(); } @@ -153,10 +162,10 @@ export default class Mailer { await this.client.mailboxOpen(`${this.ROOT_PATH}/IN`); for (const result of results) { if (result.state === 'ACCEPTED') { - console.log('accepted', result.card.uid); + this.logger.trace('accepted', result.card.uid); await this.acceptEmail(result.card.uid); } else { - console.log('rejected', result.card.uid); + this.logger.trace('rejected', result.card.uid); await this.rejectEmail(result.card.uid); } } diff --git a/src/planka.ts b/src/planka.ts index 5c29668..34f1ebf 100644 --- a/src/planka.ts +++ b/src/planka.ts @@ -10,6 +10,7 @@ import { } from '@gewis/planka-client'; import type { List } from '@gewis/planka-client'; import type { Client, Options } from '@hey-api/client-fetch'; +import log4js from 'log4js'; import type { CardEmail } from './mailer.ts'; const DEFAULT_PLANKA_URL = process.env['PLANKA_URL'] || 'http://localhost:3000'; @@ -26,8 +27,11 @@ export default class Planka { private static boardCache: Map = new Map(); + private static readonly logger = log4js.getLogger('Planka'); + private constructor(settings: { plankaUrl?: string; plankaApiKey?: string }) { this.settings = settings; + Planka.logger.level = process.env['LOG_LEVEL'] || 'info'; this.initializeClient(); } @@ -77,6 +81,7 @@ export default class Planka { * @param {CardEmail[]} cards - List of CardEmail objects to process. */ static async preProcessCards(cards: CardEmail[]) { + Planka.logger.trace('pre processing', cards.length, 'cards'); Planka.pre(); const boardIds = new Set(); for (const card of cards) { @@ -101,6 +106,7 @@ export default class Planka { const board = await getBoard({ path: { id: id.toString() } } as Options); const status = board.response.status; + Planka.logger.trace('caching board', id, 'status', status); if (status === 200 && board.data) { // Find the preferred list, which is the list named 'mail' or the first list available @@ -114,6 +120,7 @@ export default class Planka { Planka.boardCache.set(id, { board: board.data, preferredList }); } else if (status >= 400) { + Planka.logger.warn('error caching board', id, 'status', status); // Mark board as null in case of error Planka.boardCache.set(id, null); } @@ -138,6 +145,7 @@ export default class Planka { const board = Planka.boardCache.get(card.boardId); if (!board) { + Planka.logger.warn('rejecting card', card.uid, 'board not found'); // Reject card if the board is not found in the cache results.push({ card, state: 'REJECTED' }); continue; @@ -145,6 +153,7 @@ export default class Planka { const listId = card.listId || board.preferredList?.id; if (!listId) { + Planka.logger.warn('rejecting card', card.uid, 'list not found'); // Reject card if the board has no list results.push({ card, state: 'REJECTED' }); continue; @@ -160,6 +169,7 @@ export default class Planka { position: 0, }, } as Options).then(async (result) => { + Planka.logger.trace('created card', card.uid, 'status', result.response.status); const cardResult = result.data; const status = result.response.status; @@ -177,7 +187,13 @@ export default class Planka { description: card.body, dueDate: card.date ? card.date : null, }, - } as Options); + } as Options) + .then((result) => { + Planka.logger.trace('updated card', card.uid, 'status', result.response.status); + }) + .catch((e) => { + Planka.logger.error('error updating card', card.uid, e); + }); } }); } diff --git a/yarn.lock b/yarn.lock index 03b6164..f0b81b6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -718,6 +718,11 @@ data-view-byte-offset@^1.0.0: es-errors "^1.3.0" is-data-view "^1.0.1" +date-format@^4.0.14: + version "4.0.14" + resolved "https://registry.yarnpkg.com/date-format/-/date-format-4.0.14.tgz#7a8e584434fb169a521c8b7aa481f355810d9400" + integrity sha512-39BOQLs9ZjKh0/patS9nrT8wc3ioX3/eA/zgbKNopnF2wCqJEoxywwwElATYvRsXdnOxA/OQeQoFZ3rFjVajhg== + debug@^3.2.7: version "3.2.7" resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.7.tgz#72580b7e9145fb39b6676f9c5e5fb100b934179a" @@ -1246,7 +1251,7 @@ flat-cache@^4.0.0: flatted "^3.2.9" keyv "^4.5.4" -flatted@^3.2.9: +flatted@^3.2.7, flatted@^3.2.9: version "3.3.1" resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.3.1.tgz#21db470729a6734d4997002f439cb308987f567a" integrity sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw== @@ -1258,6 +1263,15 @@ for-each@^0.3.3: dependencies: is-callable "^1.1.3" +fs-extra@^8.1.0: + version "8.1.0" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-8.1.0.tgz#49d43c45a88cd9677668cb7be1b46efdb8d2e1c0" + integrity sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g== + dependencies: + graceful-fs "^4.2.0" + jsonfile "^4.0.0" + universalify "^0.1.0" + fs.realpath@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" @@ -1362,7 +1376,7 @@ gopd@^1.0.1: dependencies: get-intrinsic "^1.1.3" -graceful-fs@^4.2.4: +graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.4: version "4.2.11" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== @@ -1742,6 +1756,13 @@ jsonc-eslint-parser@^2.0.0: espree "^9.0.0" semver "^7.3.5" +jsonfile@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-4.0.0.tgz#8771aae0799b64076b76640fca058f9c10e33ecb" + integrity sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg== + optionalDependencies: + graceful-fs "^4.1.6" + "jsx-ast-utils@^2.4.1 || ^3.0.0", jsx-ast-utils@^3.3.5: version "3.3.5" resolved "https://registry.yarnpkg.com/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz#4766bd05a8e2a11af222becd19e15575e52a853a" @@ -1836,6 +1857,17 @@ lodash@^4.17.11, lodash@^4.17.21: resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== +log4js@^6.9.1: + version "6.9.1" + resolved "https://registry.yarnpkg.com/log4js/-/log4js-6.9.1.tgz#aba5a3ff4e7872ae34f8b4c533706753709e38b6" + integrity sha512-1somDdy9sChrr9/f4UlzhdaGfDR2c/SaD2a4T7qEkG4jTS57/B3qmnjLYePwQ8cqWnUHZI0iAKxMBpCZICiZ2g== + dependencies: + date-format "^4.0.14" + debug "^4.3.4" + flatted "^3.2.7" + rfdc "^1.3.0" + streamroller "^3.1.5" + loose-envify@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" @@ -2211,6 +2243,11 @@ reusify@^1.0.4: resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76" integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw== +rfdc@^1.3.0: + version "1.4.1" + resolved "https://registry.yarnpkg.com/rfdc/-/rfdc-1.4.1.tgz#778f76c4fb731d93414e8f925fbecf64cce7f6ca" + integrity sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA== + run-parallel@^1.1.9: version "1.2.0" resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.2.0.tgz#66d1368da7bdf921eb9d95bd1a9229e7f21a43ee" @@ -2336,6 +2373,15 @@ sprintf-js@^1.1.3: resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.1.3.tgz#4914b903a2f8b685d17fdf78a70e917e872e444a" integrity sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA== +streamroller@^3.1.5: + version "3.1.5" + resolved "https://registry.yarnpkg.com/streamroller/-/streamroller-3.1.5.tgz#1263182329a45def1ffaef58d31b15d13d2ee7ff" + integrity sha512-KFxaM7XT+irxvdqSP1LGLgNWbYN7ay5owZ3r/8t77p+EtSUAfUgtl7be3xtqtOmGUl9K9YPO2ca8133RlTjvKw== + dependencies: + date-format "^4.0.14" + debug "^4.3.4" + fs-extra "^8.1.0" + string.prototype.includes@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/string.prototype.includes/-/string.prototype.includes-2.0.1.tgz#eceef21283640761a81dbe16d6c7171a4edf7d92" @@ -2577,6 +2623,11 @@ undici-types@~6.19.8: resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-6.19.8.tgz#35111c9d1437ab83a7cdc0abae2f26d88eda0a02" integrity sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw== +universalify@^0.1.0: + version "0.1.2" + resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66" + integrity sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg== + uri-js@^4.2.2: version "4.4.1" resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e"